P2P Filter: Add adhoc filter option toggle (#110160)
* feat(ds): add adhoc filter option
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { getDefaultTimeRange, LoadingState } from '@grafana/data';
|
||||
import config from 'app/core/config';
|
||||
import { mockDataSource } from 'app/features/alerting/unified/mocks';
|
||||
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
@@ -16,6 +17,7 @@ import { MIXED_DATASOURCE_NAME } from '../mixed/MixedDataSource';
|
||||
import { DashboardQueryEditor, INVALID_PANEL_DESCRIPTION } from './DashboardQueryEditor';
|
||||
import { SHARED_DASHBOARD_QUERY } from './constants';
|
||||
import { DashboardDatasource } from './datasource';
|
||||
import { DashboardQuery } from './types';
|
||||
|
||||
jest.mock('app/core/config', () => ({
|
||||
...jest.requireActual('app/core/config'),
|
||||
@@ -28,6 +30,9 @@ jest.mock('app/core/config', () => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
featureToggles: {
|
||||
dashboardDsAdHocFiltering: false, // Default to false, can be overridden in tests
|
||||
},
|
||||
}));
|
||||
|
||||
setupDataSources(mockDataSource({ isDefault: true }));
|
||||
@@ -164,4 +169,62 @@ describe('DashboardQueryEditor', () => {
|
||||
INVALID_PANEL_DESCRIPTION
|
||||
);
|
||||
});
|
||||
|
||||
describe('AdHoc Filters Toggle', () => {
|
||||
beforeEach(() => {
|
||||
// Reset only the specific mocks we need, not all mocks
|
||||
mockOnChange.mockClear();
|
||||
mockOnRunQueries.mockClear();
|
||||
// Re-establish the dashboard mock in case it was cleared
|
||||
jest.spyOn(getDashboardSrv(), 'getCurrent').mockImplementation(() => mockDashboard);
|
||||
});
|
||||
|
||||
it('shows the AdHoc Filters toggle when feature toggle is enabled', async () => {
|
||||
await act(async () => {
|
||||
config.featureToggles.dashboardDsAdHocFiltering = true;
|
||||
});
|
||||
|
||||
const query: DashboardQuery = { refId: 'A', panelId: 1, adHocFiltersEnabled: false };
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<DashboardQueryEditor
|
||||
datasource={{} as DashboardDatasource}
|
||||
query={query}
|
||||
data={mockPanelData}
|
||||
onChange={mockOnChange}
|
||||
onRunQuery={mockOnRunQueries}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const adhocFiltersToggle = await screen.findByText('AdHoc Filters');
|
||||
expect(adhocFiltersToggle).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the AdHoc Filters toggle when feature toggle is disabled', async () => {
|
||||
await act(async () => {
|
||||
config.featureToggles.dashboardDsAdHocFiltering = false;
|
||||
});
|
||||
|
||||
const query: DashboardQuery = { refId: 'A', panelId: 1, adHocFiltersEnabled: false };
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<DashboardQueryEditor
|
||||
datasource={{} as DashboardDatasource}
|
||||
query={query}
|
||||
data={mockPanelData}
|
||||
onChange={mockOnChange}
|
||||
onRunQuery={mockOnRunQueries}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
// Wait for any async operations to complete
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('AdHoc Filters')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,6 +108,13 @@ export function DashboardQueryEditor({ data, query, onChange, onRunQuery }: Prop
|
||||
[query, onUpdateQuery]
|
||||
);
|
||||
|
||||
const onAdHocFiltersToggle = useCallback(() => {
|
||||
onUpdateQuery({
|
||||
...query,
|
||||
adHocFiltersEnabled: !query.adHocFiltersEnabled,
|
||||
});
|
||||
}, [query, onUpdateQuery]);
|
||||
|
||||
const isMixedDSWithDashboardQueries = (panel: PanelModel) => {
|
||||
return (
|
||||
panel.datasource?.uid === MIXED_DATASOURCE_NAME &&
|
||||
@@ -197,6 +204,16 @@ export function DashboardQueryEditor({ data, query, onChange, onRunQuery }: Prop
|
||||
<InlineSwitch value={Boolean(query.withTransforms)} onChange={onTransformToggle} />
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{config.featureToggles.dashboardDsAdHocFiltering && (
|
||||
<Field
|
||||
label="AdHoc Filters"
|
||||
description="Apply --Dashboard-- data source AdHoc filters to this panel"
|
||||
noMargin
|
||||
>
|
||||
<InlineSwitch value={Boolean(query.adHocFiltersEnabled)} onChange={onAdHocFiltersToggle} />
|
||||
</Field>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{loadingResults ? (
|
||||
|
||||
@@ -159,10 +159,10 @@ describe('DashboardDatasource', () => {
|
||||
};
|
||||
}
|
||||
|
||||
function createQueryRequest(filters: AdHocVariableFilter[], scene: SceneObject) {
|
||||
function createQueryRequest(filters: AdHocVariableFilter[], scene: SceneObject, adHocFiltersEnabled?: boolean) {
|
||||
return {
|
||||
timezone: 'utc',
|
||||
targets: [{ refId: 'A', panelId: 1 }],
|
||||
targets: [{ refId: 'A', panelId: 1, adHocFiltersEnabled }],
|
||||
requestId: '',
|
||||
interval: '',
|
||||
intervalMs: 0,
|
||||
@@ -214,7 +214,7 @@ describe('DashboardDatasource', () => {
|
||||
const ds = new DashboardDatasource({} as DataSourceInstanceSettings);
|
||||
const filters: AdHocVariableFilter[] = [{ key: 'name', operator: '=', value: 'John' }];
|
||||
|
||||
const observable = ds.query(createQueryRequest(filters, scene));
|
||||
const observable = ds.query(createQueryRequest(filters, scene, true));
|
||||
|
||||
let result: DataQueryResponse | undefined;
|
||||
observable.subscribe({ next: (data) => (result = data) });
|
||||
@@ -264,6 +264,158 @@ describe('DashboardDatasource', () => {
|
||||
expect(result?.data[0].length).toBe(3);
|
||||
});
|
||||
|
||||
it('should respect per-panel adHocFiltersEnabled setting and not filter when disabled', async () => {
|
||||
const testFrame = createTestFrame([
|
||||
{ name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] },
|
||||
{ name: 'age', type: FieldType.number, values: [25, 30, 35] },
|
||||
]);
|
||||
|
||||
const scene = new SceneFlexLayout({
|
||||
children: [
|
||||
new SceneFlexItem({
|
||||
body: new VizPanel({
|
||||
key: getVizPanelKeyForPanelId(1),
|
||||
$data: new SceneDataNode({
|
||||
data: {
|
||||
series: [testFrame],
|
||||
state: LoadingState.Done,
|
||||
timeRange: getDefaultTimeRange(),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const ds = new DashboardDatasource({} as DataSourceInstanceSettings);
|
||||
const filters: AdHocVariableFilter[] = [{ key: 'name', operator: '=', value: 'John' }];
|
||||
|
||||
// Test with adHocFiltersEnabled explicitly set to false
|
||||
const observable = ds.query(createQueryRequest(filters, scene, false));
|
||||
|
||||
let result: DataQueryResponse | undefined;
|
||||
observable.subscribe({ next: (data) => (result = data) });
|
||||
|
||||
// Should return unfiltered data since per-panel setting is disabled
|
||||
expect(result?.data[0].fields[0].values).toEqual(['John', 'Jane', 'Bob']);
|
||||
expect(result?.data[0].fields[1].values).toEqual([25, 30, 35]);
|
||||
expect(result?.data[0].length).toBe(3);
|
||||
});
|
||||
|
||||
it('should not filter when adHocFiltersEnabled is undefined (default behavior)', async () => {
|
||||
const testFrame = createTestFrame([
|
||||
{ name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] },
|
||||
{ name: 'age', type: FieldType.number, values: [25, 30, 35] },
|
||||
]);
|
||||
|
||||
const scene = new SceneFlexLayout({
|
||||
children: [
|
||||
new SceneFlexItem({
|
||||
body: new VizPanel({
|
||||
key: getVizPanelKeyForPanelId(1),
|
||||
$data: new SceneDataNode({
|
||||
data: {
|
||||
series: [testFrame],
|
||||
state: LoadingState.Done,
|
||||
timeRange: getDefaultTimeRange(),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const ds = new DashboardDatasource({} as DataSourceInstanceSettings);
|
||||
const filters: AdHocVariableFilter[] = [{ key: 'name', operator: '=', value: 'John' }];
|
||||
|
||||
// Test with adHocFiltersEnabled undefined (should default to not filtering)
|
||||
const observable = ds.query(createQueryRequest(filters, scene));
|
||||
|
||||
let result: DataQueryResponse | undefined;
|
||||
observable.subscribe({ next: (data) => (result = data) });
|
||||
|
||||
// Should return unfiltered data since adHocFiltersEnabled is not set
|
||||
expect(result?.data[0].fields[0].values).toEqual(['John', 'Jane', 'Bob']);
|
||||
expect(result?.data[0].fields[1].values).toEqual([25, 30, 35]);
|
||||
expect(result?.data[0].length).toBe(3);
|
||||
});
|
||||
|
||||
it('should apply filtering when adHocFiltersEnabled is explicitly enabled', async () => {
|
||||
const testFrame = createTestFrame([
|
||||
{ name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] },
|
||||
{ name: 'age', type: FieldType.number, values: [25, 30, 35] },
|
||||
]);
|
||||
|
||||
const scene = new SceneFlexLayout({
|
||||
children: [
|
||||
new SceneFlexItem({
|
||||
body: new VizPanel({
|
||||
key: getVizPanelKeyForPanelId(1),
|
||||
$data: new SceneDataNode({
|
||||
data: {
|
||||
series: [testFrame],
|
||||
state: LoadingState.Done,
|
||||
timeRange: getDefaultTimeRange(),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const ds = new DashboardDatasource({} as DataSourceInstanceSettings);
|
||||
const filters: AdHocVariableFilter[] = [{ key: 'name', operator: '=', value: 'John' }];
|
||||
|
||||
// Test with adHocFiltersEnabled explicitly set to true
|
||||
const observable = ds.query(createQueryRequest(filters, scene, true));
|
||||
|
||||
let result: DataQueryResponse | undefined;
|
||||
observable.subscribe({ next: (data) => (result = data) });
|
||||
|
||||
// Should return filtered data since adHocFiltersEnabled is enabled
|
||||
expect(result?.data[0].fields[0].values).toEqual(['John']);
|
||||
expect(result?.data[0].fields[1].values).toEqual([25]);
|
||||
expect(result?.data[0].length).toBe(1);
|
||||
});
|
||||
|
||||
it('should apply not-equal filtering when adHocFiltersEnabled is enabled', async () => {
|
||||
const testFrame = createTestFrame([
|
||||
{ name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] },
|
||||
{ name: 'age', type: FieldType.number, values: [25, 30, 35] },
|
||||
]);
|
||||
|
||||
const scene = new SceneFlexLayout({
|
||||
children: [
|
||||
new SceneFlexItem({
|
||||
body: new VizPanel({
|
||||
key: getVizPanelKeyForPanelId(1),
|
||||
$data: new SceneDataNode({
|
||||
data: {
|
||||
series: [testFrame],
|
||||
state: LoadingState.Done,
|
||||
timeRange: getDefaultTimeRange(),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const ds = new DashboardDatasource({} as DataSourceInstanceSettings);
|
||||
const filters: AdHocVariableFilter[] = [{ key: 'name', operator: '!=', value: 'John' }];
|
||||
|
||||
// Test with adHocFiltersEnabled explicitly set to true
|
||||
const observable = ds.query(createQueryRequest(filters, scene, true));
|
||||
|
||||
let result: DataQueryResponse | undefined;
|
||||
observable.subscribe({ next: (data) => (result = data) });
|
||||
|
||||
// Should return filtered data excluding 'John'
|
||||
expect(result?.data[0].fields[0].values).toEqual(['Jane', 'Bob']);
|
||||
expect(result?.data[0].fields[1].values).toEqual([30, 35]);
|
||||
expect(result?.data[0].length).toBe(2);
|
||||
});
|
||||
|
||||
it('should apply multiple filters with AND logic through public API', async () => {
|
||||
const testFrame = createTestFrame([
|
||||
{ name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] },
|
||||
@@ -293,7 +445,7 @@ describe('DashboardDatasource', () => {
|
||||
{ key: 'status', operator: '=', value: 'active' },
|
||||
];
|
||||
|
||||
const observable = ds.query(createQueryRequest(filters, scene));
|
||||
const observable = ds.query(createQueryRequest(filters, scene, true));
|
||||
|
||||
let result: DataQueryResponse | undefined;
|
||||
observable.subscribe({ next: (data) => (result = data) });
|
||||
@@ -598,6 +750,7 @@ describe('DashboardDatasource', () => {
|
||||
{ key: 'name', operator: '=', value: 'John' },
|
||||
{ key: 'age', operator: '!=', value: '25' },
|
||||
],
|
||||
queries: [{ refId: 'A', panelId: 1, adHocFiltersEnabled: true }],
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
@@ -606,6 +759,29 @@ describe('DashboardDatasource', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return empty array when no query has adHocFiltersEnabled enabled', async () => {
|
||||
const result = await ds.getDrilldownsApplicability({
|
||||
filters: [
|
||||
{ key: 'name', operator: '=', value: 'John' },
|
||||
{ key: 'age', operator: '!=', value: '25' },
|
||||
],
|
||||
queries: [{ refId: 'A', panelId: 1, adHocFiltersEnabled: false }],
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when queries is undefined', async () => {
|
||||
const result = await ds.getDrilldownsApplicability({
|
||||
filters: [
|
||||
{ key: 'name', operator: '=', value: 'John' },
|
||||
{ key: 'age', operator: '!=', value: '25' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should mark unsupported operators as not applicable with reason', async () => {
|
||||
const result = await ds.getDrilldownsApplicability({
|
||||
filters: [
|
||||
@@ -613,6 +789,7 @@ describe('DashboardDatasource', () => {
|
||||
{ key: 'age', operator: '<', value: '25' },
|
||||
{ key: 'score', operator: '=~', value: 'pattern' },
|
||||
],
|
||||
queries: [{ refId: 'A', panelId: 1, adHocFiltersEnabled: true }],
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
@@ -641,6 +818,7 @@ describe('DashboardDatasource', () => {
|
||||
{ key: 'age', operator: '>', value: '25' },
|
||||
{ key: 'status', operator: '!=', value: 'active' },
|
||||
],
|
||||
queries: [{ refId: 'A', panelId: 1, adHocFiltersEnabled: true }],
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
|
||||
@@ -128,10 +128,11 @@ export class DashboardDatasource extends DataSourceApi<DashboardQuery> {
|
||||
...field,
|
||||
config: {
|
||||
...field.config,
|
||||
// Enable AdHoc filtering for string and numeric fields only when feature toggle is enabled
|
||||
filterable: config.featureToggles.dashboardDsAdHocFiltering
|
||||
? field.type === FieldType.string || field.type === FieldType.number
|
||||
: field.config.filterable,
|
||||
// Enable AdHoc filtering for string and numeric fields only when feature toggle and per-panel setting are enabled
|
||||
filterable:
|
||||
config.featureToggles.dashboardDsAdHocFiltering && query.adHocFiltersEnabled
|
||||
? field.type === FieldType.string || field.type === FieldType.number
|
||||
: field.config.filterable,
|
||||
},
|
||||
state: {
|
||||
...field.state,
|
||||
@@ -140,7 +141,7 @@ export class DashboardDatasource extends DataSourceApi<DashboardQuery> {
|
||||
};
|
||||
});
|
||||
|
||||
if (!config.featureToggles.dashboardDsAdHocFiltering || filters.length === 0) {
|
||||
if (!config.featureToggles.dashboardDsAdHocFiltering || !query.adHocFiltersEnabled || filters.length === 0) {
|
||||
return [...series, ...annotations];
|
||||
}
|
||||
|
||||
@@ -348,6 +349,13 @@ export class DashboardDatasource extends DataSourceApi<DashboardQuery> {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if any query has adhoc filters enabled
|
||||
const hasAdHocFiltersEnabled = options?.queries?.some((query) => query.adHocFiltersEnabled);
|
||||
|
||||
if (!hasAdHocFiltersEnabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filters = options?.filters || [];
|
||||
|
||||
return filters.map((filter): DrilldownsApplicability => {
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface DashboardQuery extends DataQuery {
|
||||
panelId?: number;
|
||||
withTransforms?: boolean;
|
||||
topic?: DataTopic;
|
||||
adHocFiltersEnabled?: boolean;
|
||||
}
|
||||
|
||||
export type ResultInfo = {
|
||||
|
||||
Reference in New Issue
Block a user