Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions src/components/QueryEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ describe('QueryEditor', () => {
});

describe('SQL Preview with AdHoc Filters and Dashboard Variables', () => {
it('should include AdHoc filters in SQL compilation request', async () => {
it('should include dashboard AdHoc filters in SQL preview query', async () => {
// Setup mock to return AdHoc filters
mockGetTemplateSrv.mockReturnValue({
replace: jest.fn((value: string) => value),
Expand All @@ -537,20 +537,25 @@ describe('QueryEditor', () => {
setup(<QueryEditor query={query} onChange={mockOnChange} onRunQuery={mockOnRunQuery} datasource={datasource} />);

await waitFor(() => {
expect(datasource.getResource).toHaveBeenCalledWith('sql', {
query: expect.stringContaining('"filters"'),
});
expect(datasource.getResource).toHaveBeenCalledWith('sql', expect.any(Object));
});

// Parse the query to verify the filters
const callArg = (datasource.getResource as jest.Mock).mock.calls.find((call: unknown[]) => call[0] === 'sql')?.[1]
?.query;
const parsedQuery = JSON.parse(callArg);

expect(parsedQuery.filters).toEqual([
{ member: 'orders.status', operator: 'equals', values: ['completed'] },
{ member: 'orders.customer', operator: 'notEquals', values: ['test-user'] },
]);
expect(parsedQuery.filters).toHaveLength(2);
expect(parsedQuery.filters).toContainEqual({
member: 'orders.status',
operator: 'equals',
values: ['completed'],
});
expect(parsedQuery.filters).toContainEqual({
member: 'orders.customer',
operator: 'notEquals',
values: ['test-user'],
});
});

it('should include $cubeTimeDimension in SQL compilation when variable is set', async () => {
Expand Down Expand Up @@ -645,7 +650,7 @@ describe('QueryEditor', () => {
expect(parsedQuery.timeDimensions[0].granularity).toBe('day');
});

it('should combine query filters with AdHoc filters', async () => {
it('should keep query filters in SQL compilation request', async () => {
// Setup mock to return AdHoc filters
mockGetTemplateSrv.mockReturnValue({
replace: jest.fn((value: string) => value),
Expand All @@ -668,7 +673,7 @@ describe('QueryEditor', () => {
expect(datasource.getResource).toHaveBeenCalled();
});

// Parse the query to verify both filters are included
// Parse the query to verify query-level and dashboard AdHoc filters are included
const callArg = (datasource.getResource as jest.Mock).mock.calls.find((call: unknown[]) => call[0] === 'sql')?.[1]
?.query;
const parsedQuery = JSON.parse(callArg);
Expand Down
6 changes: 5 additions & 1 deletion src/components/QueryEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
import { InlineField, Input, Alert, MultiSelect, Text, Field, useStyles2 } from '@grafana/ui';
import { css } from '@emotion/css';
import { GrafanaTheme2, QueryEditorProps } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { DataSource } from '../datasource';
import { CubeDataSourceOptions, CubeQuery } from '../types';
import { SQLPreview } from './SQLPreview';
Expand All @@ -18,7 +19,10 @@ export function QueryEditor({
datasource,
}: QueryEditorProps<DataSource, CubeQuery, CubeDataSourceOptions>) {
const styles = useStyles2(getStyles);
const cubeQueryJson = useMemo(() => buildCubeQueryJson(query, datasource), [query, datasource]);
const adHocFilters = useMemo(() => {
return getTemplateSrv().getAdhocFilters(datasource.name) as NonNullable<Parameters<typeof buildCubeQueryJson>[2]>;
Comment thread
cursor[bot] marked this conversation as resolved.
}, [datasource.name]);
const cubeQueryJson = useMemo(() => buildCubeQueryJson(query, datasource, adHocFilters), [query, datasource, adHocFilters]);
Comment thread
cursor[bot] marked this conversation as resolved.

const { data, isLoading: metadataIsLoading, isError: metadataIsError } = useMetadataQuery({ datasource });
const metadata = data ?? { dimensions: [], measures: [] };
Expand Down
64 changes: 32 additions & 32 deletions src/datasource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,14 +334,7 @@ describe('DataSource', () => {

mockGetTemplateSrv.mockReturnValue({
replace: mockReplace,
getAdhocFilters: () => [
{
key: 'orders.status',
operator: '=|',
value: 'completed',
values: ['completed', 'shipped', 'delivered'],
},
],
getAdhocFilters: () => [],
});

const datasource = createDataSource();
Expand All @@ -352,7 +345,14 @@ describe('DataSource', () => {
measures: ['orders.count'],
};

const result = datasource.applyTemplateVariables(query, {});
const result = datasource.applyTemplateVariables(query, {}, [
{
key: 'orders.status',
operator: '=|',
value: 'completed',
values: ['completed', 'shipped', 'delivered'],
},
]);

expect(result.filters).toBeDefined();
expect(result.filters).toHaveLength(1);
Expand All @@ -368,14 +368,7 @@ describe('DataSource', () => {

mockGetTemplateSrv.mockReturnValue({
replace: mockReplace,
getAdhocFilters: () => [
{
key: 'orders.status',
operator: '!=|',
value: 'cancelled',
values: ['cancelled', 'refunded'],
},
],
getAdhocFilters: () => [],
});

const datasource = createDataSource();
Expand All @@ -386,7 +379,14 @@ describe('DataSource', () => {
measures: ['orders.count'],
};

const result = datasource.applyTemplateVariables(query, {});
const result = datasource.applyTemplateVariables(query, {}, [
{
key: 'orders.status',
operator: '!=|',
value: 'cancelled',
values: ['cancelled', 'refunded'],
},
]);

expect(result.filters).toBeDefined();
expect(result.filters).toHaveLength(1);
Expand All @@ -402,14 +402,7 @@ describe('DataSource', () => {

mockGetTemplateSrv.mockReturnValue({
replace: mockReplace,
getAdhocFilters: () => [
{
key: 'orders.status',
operator: '=',
value: 'completed',
values: [], // Empty values array
},
],
getAdhocFilters: () => [],
});

const datasource = createDataSource();
Expand All @@ -420,7 +413,14 @@ describe('DataSource', () => {
measures: ['orders.count'],
};

const result = datasource.applyTemplateVariables(query, {});
const result = datasource.applyTemplateVariables(query, {}, [
{
key: 'orders.status',
operator: '=',
value: 'completed',
values: [], // Empty values array
},
]);

expect(result.filters).toBeDefined();
expect(result.filters![0].values).toEqual(['completed']);
Expand All @@ -431,10 +431,7 @@ describe('DataSource', () => {

mockGetTemplateSrv.mockReturnValue({
replace: mockReplace,
getAdhocFilters: () => [
{ key: 'orders.status', operator: '=', value: 'completed' },
{ key: 'orders.customer', operator: '!=', value: 'test' },
],
getAdhocFilters: () => [],
});

const datasource = createDataSource();
Expand All @@ -445,7 +442,10 @@ describe('DataSource', () => {
measures: ['orders.count'],
};

const result = datasource.applyTemplateVariables(query, {});
const result = datasource.applyTemplateVariables(query, {}, [
{ key: 'orders.status', operator: '=', value: 'completed' },
{ key: 'orders.customer', operator: '!=', value: 'test' },
]);

expect(result.filters).toHaveLength(2);
expect(result.filters![0].operator).toBe('equals');
Expand Down
25 changes: 15 additions & 10 deletions src/datasource.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DataSourceInstanceSettings, CoreApp, ScopedVars } from '@grafana/data';
import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime';

import { CubeQuery, CubeDataSourceOptions, DEFAULT_QUERY, CubeFilter } from './types';
import { CubeQuery, CubeDataSourceOptions, DEFAULT_QUERY, CubeFilter, Operator } from './types';
import { filterValidCubeFilters } from './utils/filterValidation';

export class DataSource extends DataSourceWithBackend<CubeQuery, CubeDataSourceOptions> {
Expand All @@ -16,7 +16,11 @@ export class DataSource extends DataSourceWithBackend<CubeQuery, CubeDataSourceO
return DEFAULT_QUERY;
}

applyTemplateVariables(query: CubeQuery, scopedVars: ScopedVars): CubeQuery {
applyTemplateVariables(
query: CubeQuery,
scopedVars: ScopedVars,
adHocFiltersArg?: Array<{ key: string; operator: string; value: string; values?: string[] }>
): CubeQuery {
const templateSrv = getTemplateSrv();

// Dimensions and measures: pass through as-is (no interpolation of dashboard variables)
Expand All @@ -33,8 +37,9 @@ export class DataSource extends DataSourceWithBackend<CubeQuery, CubeDataSourceO
values: filter.values.map((v) => templateSrv.replace(v, scopedVars)),
}));

// Check for AdHoc filters and inject them
const adHocFilters = (templateSrv as any).getAdhocFilters ? (templateSrv as any).getAdhocFilters(this.name) : [];
// AdHoc filters are provided by Grafana on the request path via applyTemplateVariables(..., filters).
// Avoid templateSrv.getAdhocFilters(), which is deprecated.
const adHocFilters = adHocFiltersArg ?? [];

let filters = interpolatedFilters ? [...interpolatedFilters] : [];

Expand Down Expand Up @@ -106,24 +111,24 @@ export class DataSource extends DataSourceWithBackend<CubeQuery, CubeDataSourceO
}

// Made public so QueryEditor can use this for SQL preview with AdHoc filters
mapOperator(grafanaOp: string): string {
mapOperator(grafanaOp: string): CubeFilter['operator'] {
switch (grafanaOp) {
case '=':
case '=|': // "One of" - Cube's equals operator supports multiple values
return 'equals';
return Operator.Equals;
case '!=':
case '!=|': // "Not one of" - Cube's notEquals operator supports multiple values
return 'notEquals';
return Operator.NotEquals;
// Note: =~ and !~ are Prometheus regex operators, not substring contains.
// We intentionally don't (yet) map these to `contains` or `notContains` to avoid semantic confusion,
// and because isValidCubeFilter doesn't support `contains` or `notContains` yet.
// We don't yet test for the behaviour below because it's not desirable long term - it's a temporary workaround.
case '=~':
return 'equals';
return Operator.Equals;
case '!~':
return 'notEquals';
return Operator.NotEquals;
default:
return 'equals';
return Operator.Equals;
}
}

Expand Down
13 changes: 7 additions & 6 deletions src/utils/buildCubeQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import { normalizeOrder } from './normalizeOrder';
* This function uses @cubejs-client/core types to ensure compile-time
* compatibility with Cube's /load endpoint format.
*/
export function buildCubeQueryJson(query: CubeQuery, datasource: DataSource): string {
export function buildCubeQueryJson(
query: CubeQuery,
datasource: DataSource,
adHocFiltersArg?: Array<{ key: string; operator: string; value: string; values?: string[] }>
): string {
if (!query.dimensions?.length && !query.measures?.length) {
return '';
}
Expand Down Expand Up @@ -73,11 +77,8 @@ export function buildCubeQueryJson(query: CubeQuery, datasource: DataSource): st
// Combine query-level filters with AdHoc filters
let filters: CubeFilter[] = query.filters?.length ? [...query.filters] : [];

// Get AdHoc filters and convert to Cube format
const templateSrv = getTemplateSrv();
const adHocFilters = (templateSrv as any).getAdhocFilters
? (templateSrv as any).getAdhocFilters(datasource.name)
: [];
// AdHoc filters may be provided by the caller (for example from request context).
const adHocFilters = adHocFiltersArg ?? [];
Comment thread
cursor[bot] marked this conversation as resolved.

if (adHocFilters && adHocFilters.length > 0) {
const cubeFilters: CubeFilter[] = adHocFilters.map((filter: any) => ({
Expand Down
Loading