Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
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
112 changes: 34 additions & 78 deletions src/datasource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ describe('DataSource', () => {
});

describe('applyTemplateVariables', () => {
beforeEach(() => {
mockGetTemplateSrv.mockReturnValue({
replace: (str: string) => str,
});
});

it('should interpolate template variables in filter values', () => {
const mockReplace = jest.fn((str: string) => {
if (str === '$filterValue') {
Expand All @@ -171,7 +177,6 @@ describe('DataSource', () => {

mockGetTemplateSrv.mockReturnValue({
replace: mockReplace,
getAdhocFilters: () => [],
});

const datasource = createDataSource();
Expand Down Expand Up @@ -215,7 +220,6 @@ describe('DataSource', () => {

mockGetTemplateSrv.mockReturnValue({
replace: mockReplace,
getAdhocFilters: () => [],
});

const datasource = createDataSource();
Expand Down Expand Up @@ -252,7 +256,6 @@ describe('DataSource', () => {

mockGetTemplateSrv.mockReturnValue({
replace: mockReplace,
getAdhocFilters: () => [],
});

const datasource = createDataSource();
Expand All @@ -277,16 +280,6 @@ describe('DataSource', () => {
});

it('should not inject time dimension when $cubeTimeDimension variable is not set', () => {
const mockReplace = jest.fn((str: string) => {
// Return the variable name unchanged when not set
return str;
});

mockGetTemplateSrv.mockReturnValue({
replace: mockReplace,
getAdhocFilters: () => [],
});

const datasource = createDataSource();

const query = {
Expand All @@ -311,7 +304,6 @@ describe('DataSource', () => {

mockGetTemplateSrv.mockReturnValue({
replace: mockReplace,
getAdhocFilters: () => [],
});

const datasource = createDataSource();
Expand All @@ -330,20 +322,6 @@ describe('DataSource', () => {

describe('AdHoc filter operators', () => {
it('should map "One of" operator (=|) to Cube equals with multiple values', () => {
const mockReplace = jest.fn((str: string) => str);

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

const datasource = createDataSource();

const query = {
Expand All @@ -352,7 +330,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 @@ -364,20 +349,6 @@ describe('DataSource', () => {
});

it('should map "Not one of" operator (!=|) to Cube notEquals with multiple values', () => {
const mockReplace = jest.fn((str: string) => str);

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

const datasource = createDataSource();

const query = {
Expand All @@ -386,7 +357,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 @@ -398,20 +376,6 @@ describe('DataSource', () => {
});

it('should fall back to single value when values array is empty', () => {
const mockReplace = jest.fn((str: string) => str);

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

const datasource = createDataSource();

const query = {
Expand All @@ -420,23 +384,20 @@ 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']);
});

it('should handle standard single-value operators', () => {
const mockReplace = jest.fn((str: string) => str);

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

const datasource = createDataSource();

const query = {
Expand All @@ -445,7 +406,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 All @@ -454,14 +418,6 @@ describe('DataSource', () => {
});

describe('filter validation', () => {
beforeEach(() => {
// Reset template srv mock to avoid AdHoc filters from previous tests
mockGetTemplateSrv.mockReturnValue({
replace: (str: string) => str,
getAdhocFilters: () => [],
});
});

it('should strip out filters with empty values', () => {
const datasource = createDataSource();

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