Skip to content

Commit c6a0be4

Browse files
committed
Add key transpose to ad-hoc filters
1 parent 39175f1 commit c6a0be4

5 files changed

Lines changed: 81 additions & 38 deletions

File tree

src/data/CHDatasource.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ describe('ClickHouseDatasource', () => {
110110
} as CHQuery;
111111

112112
// Mock the ad-hoc filter
113-
const adHocFilter = new AdHocFilter();
113+
const adHocFilter = new AdHocFilter(null);
114114

115115
// The resolved table name after template variable substitution
116116
const resolvedSql = 'SELECT * FROM test_db.test_table';

src/data/CHDatasource.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export class Datasource
7171
constructor(instanceSettings: DataSourceInstanceSettings<CHConfig>) {
7272
super(instanceSettings);
7373
this.settings = instanceSettings;
74-
this.adHocFilter = new AdHocFilter();
74+
this.adHocFilter = new AdHocFilter(this);
7575
}
7676

7777
getDataProvider(
@@ -851,6 +851,14 @@ export class Datasource
851851
}
852852

853853
async getTagKeys(): Promise<MetricFindValue[]> {
854+
855+
const TRANSPOSE_KEYS_VAR = '$clickhouse_transpose_keys';
856+
const TRANSPOSE_KEY_PREFIX_VAR = '$clickhouse_transpose_keys_prefix';
857+
const transposeKeysToRows: boolean = JSON.parse(getTemplateSrv().replace(TRANSPOSE_KEYS_VAR)) || false;
858+
const transposeKeysPrefix: string = getTemplateSrv().replace(TRANSPOSE_KEY_PREFIX_VAR) || '';
859+
860+
console.log(`Transpose: ${transposeKeysToRows}`);
861+
854862
if (this.adHocFiltersStatus === AdHocFilterStatus.disabled || this.adHocFiltersStatus === AdHocFilterStatus.none) {
855863
this.adHocFiltersStatus = await this.canUseAdhocFilters();
856864
if (this.adHocFiltersStatus === AdHocFilterStatus.disabled) {
@@ -859,8 +867,17 @@ export class Datasource
859867
}
860868
const { type, frame } = await this.fetchTags();
861869
if (type === TagType.query) {
862-
return frame.fields.map((f) => ({ text: f.name }));
870+
871+
if(!transposeKeysToRows) {
872+
return frame.fields.map((f) => ({ text: f.name }));
873+
}
874+
875+
const view = new DataFrameView(frame);
876+
return view.fields?.key.values.map((key) => ({
877+
text: `${transposeKeysPrefix}.${key}`
878+
}));
863879
}
880+
864881
const view = new DataFrameView(frame);
865882
const hideTableName = this.settings.jsonData.hideTableNameInAdhocFilters || false;
866883
return view.map((item) => ({
@@ -975,6 +992,8 @@ export class Datasource
975992
}
976993
}
977994

995+
console.log(`Running query: ${tagSource?.source}`);
996+
978997
const results = await this.runQuery({ rawSql: tagSource.source });
979998
return { type: tagSource.type, frame: results };
980999
}

src/data/adHocFilter.test.ts

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { AdHocFilter } from './adHocFilter';
33

44
describe('AdHocManager', () => {
55
it('apply ad hoc filter with no inner query and existing WHERE', () => {
6-
const ahm = new AdHocFilter();
6+
const ahm = new AdHocFilter(null);
77
ahm.setTargetTableFromQuery('SELECT * FROM foo');
88
const val = ahm.apply('SELECT stuff FROM foo WHERE col = test', [
99
{ key: 'key', operator: '=', value: 'val' },
@@ -14,7 +14,7 @@ describe('AdHocManager', () => {
1414
);
1515
});
1616
it('apply ad hoc filter with no inner query and no existing WHERE', () => {
17-
const ahm = new AdHocFilter();
17+
const ahm = new AdHocFilter(null);
1818
ahm.setTargetTableFromQuery('SELECT * FROM foo');
1919
const val = ahm.apply('SELECT stuff FROM foo', [
2020
{ key: 'key', operator: '=', value: 'val' },
@@ -25,7 +25,7 @@ describe('AdHocManager', () => {
2525
);
2626
});
2727
it('apply ad hoc filter with an inner query without existing WHERE', () => {
28-
const ahm = new AdHocFilter();
28+
const ahm = new AdHocFilter(null);
2929
ahm.setTargetTableFromQuery('SELECT * FROM foo');
3030
const val = ahm.apply(`SELECT stuff FROM (SELECT * FROM foo) as r , bar GROUP BY s ORDER BY s`, [
3131
{ key: 'key', operator: '=', value: 'val' },
@@ -36,7 +36,7 @@ describe('AdHocManager', () => {
3636
);
3737
});
3838
it('apply ad hoc filter with an inner from query with existing WHERE', () => {
39-
const ahm = new AdHocFilter();
39+
const ahm = new AdHocFilter(null);
4040
ahm.setTargetTableFromQuery('SELECT * FROM foo');
4141
const val = ahm.apply(`SELECT stuff FROM (SELECT * FROM foo WHERE col = test) as r GROUP BY s ORDER BY s`, [
4242
{ key: 'key', operator: '=', value: 'val' },
@@ -47,7 +47,7 @@ describe('AdHocManager', () => {
4747
);
4848
});
4949
it('apply ad hoc filter with an inner where query with existing WHERE', () => {
50-
const ahm = new AdHocFilter();
50+
const ahm = new AdHocFilter(null);
5151
ahm.setTargetTableFromQuery('SELECT * FROM foo');
5252
const val = ahm.apply(
5353
`SELECT * FROM foo WHERE (name = stuff) AND (name IN ( SELECT * FROM foo WHERE (field = 'hello') GROUP BY name ORDER BY count() DESC LIMIT 10 )) GROUP BY name , time ORDER BY time`,
@@ -58,23 +58,23 @@ describe('AdHocManager', () => {
5858
);
5959
});
6060
it('does not apply ad hoc filter when the target table is not in the query', () => {
61-
const ahm = new AdHocFilter();
61+
const ahm = new AdHocFilter(null);
6262
ahm.setTargetTableFromQuery('SELECT * FROM bar');
6363
const val = ahm.apply('select stuff FROM foo', [
6464
{ key: 'key', operator: '=', value: 'val' },
6565
] as AdHocVariableFilter[]);
6666
expect(val).toEqual('select stuff FROM foo');
6767
});
6868
it('apply ad hoc filter when the ad hoc options are from a query with a from inline query', () => {
69-
const ahm = new AdHocFilter();
69+
const ahm = new AdHocFilter(null);
7070
ahm.setTargetTableFromQuery('SELECT * FROM (select * FROM foo) bar');
7171
const val = ahm.apply('select stuff FROM foo', [
7272
{ key: 'key', operator: '=', value: 'val' },
7373
] as AdHocVariableFilter[]);
7474
expect(val).toEqual(`select stuff FROM foo settings additional_table_filters={'foo' : ' key = \\'val\\' '}`);
7575
});
7676
it('apply ad hoc filter when the ad hoc options are from a query with a where inline query', () => {
77-
const ahm = new AdHocFilter();
77+
const ahm = new AdHocFilter(null);
7878
ahm.setTargetTableFromQuery(
7979
'SELECT * FROM foo where stuff = stuff and (repo in (select * FROM foo)) order by stuff'
8080
);
@@ -84,7 +84,7 @@ describe('AdHocManager', () => {
8484
expect(val).toEqual(`select stuff FROM foo settings additional_table_filters={'foo' : ' key = \\'val\\' '}`);
8585
});
8686
it('apply ad hoc filter to complex join statement', () => {
87-
const ahm = new AdHocFilter();
87+
const ahm = new AdHocFilter(null);
8888
ahm.setTargetTableFromQuery(
8989
'SELECT * FROM foo where stuff = stuff and (repo in (select * FROM foo)) order by stuff'
9090
);
@@ -97,13 +97,13 @@ describe('AdHocManager', () => {
9797
);
9898
});
9999
it('throws an error when the adhoc filter select cannot be parsed', () => {
100-
const ahm = new AdHocFilter();
100+
const ahm = new AdHocFilter(null);
101101
expect(function () {
102102
ahm.setTargetTableFromQuery('select not sql');
103103
}).toThrow(new Error('Failed to get table from adhoc query.'));
104104
});
105105
it('apply ad hoc filter with same table casing', () => {
106-
const ahm = new AdHocFilter();
106+
const ahm = new AdHocFilter(null);
107107
ahm.setTargetTableFromQuery('SELECT * FROM fooTable');
108108
const val = ahm.apply('SELECT stuff FROM fooTable', [
109109
{ key: 'key', operator: '=', value: 'val' },
@@ -113,7 +113,7 @@ describe('AdHocManager', () => {
113113
);
114114
});
115115
it('apply ad hoc filter with default schema', () => {
116-
const ahm = new AdHocFilter();
116+
const ahm = new AdHocFilter(null);
117117
ahm.setTargetTableFromQuery('SELECT * FROM default.foo');
118118
const val = ahm.apply('SELECT stuff FROM default.foo', [
119119
{ key: 'key', operator: '=', value: 'val' },
@@ -123,7 +123,7 @@ describe('AdHocManager', () => {
123123
);
124124
});
125125
it('apply ad hoc filter and does not include the table reference in the selected fields of the function', () => {
126-
const ahm = new AdHocFilter();
126+
const ahm = new AdHocFilter(null);
127127
ahm.setTargetTableFromQuery('SELECT * FROM foo');
128128
const val = ahm.apply('SELECT foo.stuff FROM foo', [
129129
{ key: 'foo.key', operator: '=', value: 'val' },
@@ -132,7 +132,7 @@ describe('AdHocManager', () => {
132132
});
133133

134134
it('apply ad hoc filter converts "=~" to "ILIKE"', () => {
135-
const ahm = new AdHocFilter();
135+
const ahm = new AdHocFilter(null);
136136
ahm.setTargetTableFromQuery('SELECT * FROM foo');
137137
const val = ahm.apply('SELECT stuff FROM foo WHERE col = test', [
138138
{ key: 'key', operator: '=~', value: 'val' },
@@ -143,7 +143,7 @@ describe('AdHocManager', () => {
143143
});
144144

145145
it('apply ad hoc filter converts "!~" to "NOT ILIKE"', () => {
146-
const ahm = new AdHocFilter();
146+
const ahm = new AdHocFilter(null);
147147
ahm.setTargetTableFromQuery('SELECT * FROM foo');
148148
const val = ahm.apply('SELECT stuff FROM foo WHERE col = test', [
149149
{ key: 'key', operator: '!~', value: 'val' },
@@ -154,7 +154,7 @@ describe('AdHocManager', () => {
154154
});
155155

156156
it('apply ad hoc filter IN operator with string values', () => {
157-
const ahm = new AdHocFilter();
157+
const ahm = new AdHocFilter(null);
158158
ahm.setTargetTableFromQuery('SELECT * FROM foo');
159159
const val = ahm.apply('SELECT stuff FROM foo WHERE col = test', [
160160
{ key: 'key', operator: 'IN', value: "('val1', 'val2')" },
@@ -165,7 +165,7 @@ describe('AdHocManager', () => {
165165
});
166166

167167
it('apply ad hoc filter IN operator without parentheses', () => {
168-
const ahm = new AdHocFilter();
168+
const ahm = new AdHocFilter(null);
169169
ahm.setTargetTableFromQuery('SELECT * FROM foo');
170170
const val = ahm.apply('SELECT stuff FROM foo WHERE col = test', [
171171
{ key: 'key', operator: 'IN', value: "'val1', 'val2'" },
@@ -176,7 +176,7 @@ describe('AdHocManager', () => {
176176
});
177177

178178
it('apply ad hoc filter IN operator with integer values', () => {
179-
const ahm = new AdHocFilter();
179+
const ahm = new AdHocFilter(null);
180180
ahm.setTargetTableFromQuery('SELECT * FROM foo');
181181
const val = ahm.apply('SELECT stuff FROM foo WHERE col = test', [
182182
{ key: 'key', operator: 'IN', value: '(1, 2, 3)' },
@@ -187,7 +187,7 @@ describe('AdHocManager', () => {
187187
});
188188

189189
it('does not apply an adhoc filter without "operator"', () => {
190-
const ahm = new AdHocFilter();
190+
const ahm = new AdHocFilter(null);
191191
ahm.setTargetTableFromQuery('SELECT * FROM foo');
192192
const val = ahm.apply('SELECT foo.stuff FROM foo', [
193193
// @ts-expect-error
@@ -197,7 +197,7 @@ describe('AdHocManager', () => {
197197
});
198198

199199
it('does not apply an adhoc filter without "value"', () => {
200-
const ahm = new AdHocFilter();
200+
const ahm = new AdHocFilter(null);
201201
ahm.setTargetTableFromQuery('SELECT * FROM foo');
202202
const val = ahm.apply('SELECT foo.stuff FROM foo', [
203203
// @ts-expect-error
@@ -207,7 +207,7 @@ describe('AdHocManager', () => {
207207
});
208208

209209
it('does not apply an adhoc filter without "key"', () => {
210-
const ahm = new AdHocFilter();
210+
const ahm = new AdHocFilter(null);
211211
ahm.setTargetTableFromQuery('SELECT * FROM foo');
212212
const val = ahm.apply('SELECT foo.stuff FROM foo', [
213213
// @ts-expect-error
@@ -219,7 +219,7 @@ describe('AdHocManager', () => {
219219
it('log a malformed filter', () => {
220220
const warn = jest.spyOn(console, 'warn');
221221
const value = { key: 'foo.key', operator: '=', value: undefined };
222-
const ahm = new AdHocFilter();
222+
const ahm = new AdHocFilter(null);
223223
ahm.setTargetTableFromQuery('SELECT * FROM foo');
224224
ahm.apply('SELECT foo.stuff FROM foo', [
225225
// @ts-expect-error
@@ -230,23 +230,23 @@ describe('AdHocManager', () => {
230230
});
231231

232232
it('apply ad hoc filter with no set table', () => {
233-
const ahm = new AdHocFilter();
233+
const ahm = new AdHocFilter(null);
234234
const val = ahm.apply('SELECT stuff FROM foo', [
235235
{ key: 'key', operator: '=', value: 'val' },
236236
] as AdHocVariableFilter[]);
237237
expect(val).toEqual(`SELECT stuff FROM foo settings additional_table_filters={'foo' : ' key = \\'val\\' '}`);
238238
});
239239

240240
it('converts arrayElement with single quotes', () => {
241-
const ahm = new AdHocFilter();
241+
const ahm = new AdHocFilter(null);
242242
const result = ahm.apply('SELECT * FROM foo', [
243243
{ key: "arrayElement(ResourceAttributes, 'cloud.region')", operator: '=', value: 'test' },
244244
] as AdHocVariableFilter[]);
245245
expect(result).toContain("ResourceAttributes[\\'cloud.region\\']");
246246
});
247247

248248
it('converts arrayElement with single quotes', () => {
249-
const ahm = new AdHocFilter();
249+
const ahm = new AdHocFilter(null);
250250
const result = ahm.apply('SELECT * FROM foo', [
251251
{ key: "ResourceAttributes.cloud.region'", operator: '=', value: 'test' },
252252
] as AdHocVariableFilter[]);
@@ -255,13 +255,13 @@ describe('AdHocManager', () => {
255255

256256
describe('buildFilterString', () => {
257257
it('builds filter string with single filter', () => {
258-
const ahm = new AdHocFilter();
258+
const ahm = new AdHocFilter(null);
259259
const result = ahm.buildFilterString([{ key: 'key', operator: '=', value: 'val' }] as AdHocVariableFilter[]);
260260
expect(result).toEqual(" key = \\'val\\' ");
261261
});
262262

263263
it('builds filter string with multiple filters', () => {
264-
const ahm = new AdHocFilter();
264+
const ahm = new AdHocFilter(null);
265265
const result = ahm.buildFilterString([
266266
{ key: 'key', operator: '=', value: 'val' },
267267
{ key: 'keyNum', operator: '=', value: '123' },
@@ -270,27 +270,27 @@ describe('AdHocManager', () => {
270270
});
271271

272272
it('returns empty string with no filters', () => {
273-
const ahm = new AdHocFilter();
273+
const ahm = new AdHocFilter(null);
274274
const result = ahm.buildFilterString([]);
275275
expect(result).toEqual('');
276276
});
277277

278278
it('builds filter string with regex operators', () => {
279-
const ahm = new AdHocFilter();
279+
const ahm = new AdHocFilter(null);
280280
const result = ahm.buildFilterString([{ key: 'key', operator: '=~', value: 'val' }] as AdHocVariableFilter[]);
281281
expect(result).toEqual(" key ILIKE \\'val\\' ");
282282
});
283283

284284
it('builds filter string with IN operator', () => {
285-
const ahm = new AdHocFilter();
285+
const ahm = new AdHocFilter(null);
286286
const result = ahm.buildFilterString([
287287
{ key: 'key', operator: 'IN', value: "'val1', 'val2'" },
288288
] as AdHocVariableFilter[]);
289289
expect(result).toEqual(" key IN (\\'val1\\', \\'val2\\') ");
290290
});
291291

292292
it('ignores invalid filters', () => {
293-
const ahm = new AdHocFilter();
293+
const ahm = new AdHocFilter(null);
294294
const result = ahm.buildFilterString([
295295
{ key: 'key', operator: '=', value: 'val' },
296296
{ key: '', operator: '=', value: 'val' } as any,
@@ -300,7 +300,7 @@ describe('AdHocManager', () => {
300300
});
301301
});
302302
it('should apply ad hoc filter with . in column name', () => {
303-
const ahm = new AdHocFilter();
303+
const ahm = new AdHocFilter(null);
304304
const val = ahm.apply('SELECT stuff FROM foo', [
305305
{ key: 'TABLE.key.key2', operator: '=', value: 'val' },
306306
] as AdHocVariableFilter[]);

src/data/adHocFilter.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
11
import { AdHocVariableFilter } from '@grafana/data';
22
import { getTable } from './ast';
3+
import { Datasource } from './CHDatasource';
4+
5+
6+
interface AdhocFiltersConfig {
7+
hideTableNameInAdhocFilters?: boolean;
8+
};
39

410
export class AdHocFilter {
511
private _targetTable = '';
12+
private _datasourceInstance: Datasource | null = null;
13+
private _config: AdhocFiltersConfig = {}
14+
15+
constructor(instance: Datasource | null) {
16+
this._datasourceInstance = instance
17+
if (!this._datasourceInstance) {
18+
throw new Error(`Datasource instance was null - unable to instantiate AdHocFilter!`)
19+
}
20+
21+
this._config.hideTableNameInAdhocFilters = instance?.settings.jsonData.hideTableNameInAdhocFilters;
22+
}
23+
624

725
setTargetTableFromQuery(query: string) {
826
this._targetTable = getTable(query);
@@ -26,7 +44,7 @@ export class AdHocFilter {
2644

2745
const filters = validFilters
2846
.map((f, i) => {
29-
const key = escapeKey(f.key);
47+
const key = escapeKey(this._config, f.key);
3048
const value = escapeValueBasedOnOperator(f.value, f.operator);
3149
const condition = i !== validFilters.length - 1 ? (f.condition ? f.condition : 'AND') : '';
3250
const operator = convertOperatorToClickHouseOperator(f.operator);
@@ -70,7 +88,7 @@ function isValid(filter: AdHocVariableFilter): boolean {
7088
return filter.key !== undefined && filter.key !== '' && filter.operator !== undefined && filter.value !== undefined;
7189
}
7290

73-
function escapeKey(s: string): string {
91+
function escapeKey(opts: AdhocFiltersConfig, s: string): string {
7492
if (['ResourceAttributes', 'ScopeAttributes', 'LogAttributes'].includes(s.split('.')[0])) {
7593
return s;
7694
}
@@ -83,6 +101,11 @@ function escapeKey(s: string): string {
83101
return `${array}[\\'${key}\\']`;
84102
}
85103
}
104+
105+
const hideTableNameInAdhocFilters = opts?.hideTableNameInAdhocFilters || false;
106+
if (hideTableNameInAdhocFilters) {
107+
return s;
108+
}
86109
return s.includes('.') ? s.split('.').slice(1).join('.') : s;
87110
}
88111

0 commit comments

Comments
 (0)