Skip to content

Commit

Permalink
QueryVariable: Support for queries that contain "$__searchFilter" (#395)
Browse files Browse the repository at this point in the history
  • Loading branch information
torkelo authored Oct 6, 2023
1 parent b9215b5 commit 1034177
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 55 deletions.
1 change: 1 addition & 0 deletions docusaurus/docs/variables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function getVariablesScene() {
},
query: {
query: 'label_values(prometheus_http_requests_total,handler)',
refId: 'A',
},
});

Expand Down
33 changes: 33 additions & 0 deletions packages/scenes-app/src/demos/variables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
DataSourceVariable,
SceneQueryRunner,
TextBoxVariable,
QueryVariable,
} from '@grafana/scenes';
import { getQueryRunnerWithRandomWalkQuery } from './utils';

Expand Down Expand Up @@ -143,6 +144,38 @@ export function getVariablesDemo(defaults: SceneAppPageState) {
});
},
}),
new SceneAppPage({
title: 'Search filter',
url: `${defaults.url}/search`,
getScene: () => {
return new EmbeddedScene({
controls: [new VariableValueSelectors({})],
$variables: new SceneVariableSet({
variables: [
new QueryVariable({
name: 'server',
query: { query: 'A.$__searchFilter', refId: 'A' },
datasource: { uid: 'gdev-testdata' },
}),
],
}),
body: new SceneFlexLayout({
direction: 'column',
children: [
new SceneFlexItem({
body: PanelBuilders.text()
.setTitle('Variable with search filter')
.setOption(
'content',
'This is a very old messy feature that allows data sources to filter down the options in a query variable dropdown based on what the user has typed in. Only implemented by very few data sources (Graphite, SQL, Datadog)'
)
.build(),
}),
],
}),
});
},
}),
],
});
}
Expand Down
11 changes: 1 addition & 10 deletions packages/scenes/src/variables/VariableDependencyConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SceneObject, SceneObjectState } from '../core/types';
import { VARIABLE_REGEX } from './constants';

import { SceneVariable, SceneVariableDependencyConfigLike } from './types';
import { safeStringifyValue } from './utils';

interface VariableDependencyConfigOptions<TState extends SceneObjectState> {
/**
Expand Down Expand Up @@ -152,13 +153,3 @@ export class VariableDependencyConfig<TState extends SceneObjectState> implement
}
}
}

const safeStringifyValue = (value: unknown) => {
try {
return JSON.stringify(value, null);
} catch (error) {
console.error(error);
}

return '';
};
21 changes: 19 additions & 2 deletions packages/scenes/src/variables/components/VariableValueSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isArray } from 'lodash';
import React from 'react';

import { MultiSelect, Select } from '@grafana/ui';
import { InputActionMeta, MultiSelect, Select } from '@grafana/ui';

import { SceneComponentProps } from '../../core/types';
import { MultiValueVariable } from '../variants/MultiValueVariable';
Expand All @@ -10,6 +10,14 @@ import { VariableValue, VariableValueSingle } from '../types';
export function VariableValueSelect({ model }: SceneComponentProps<MultiValueVariable>) {
const { value, key } = model.useState();

const onInputChange = model.onSearchChange
? (value: string, meta: InputActionMeta) => {
if (meta.action === 'input-change') {
model.onSearchChange!(value);
}
}
: undefined;

return (
<Select<VariableValue>
id={key}
Expand All @@ -18,6 +26,7 @@ export function VariableValueSelect({ model }: SceneComponentProps<MultiValueVar
value={value}
allowCustomValue
tabSelectsValue={false}
onInputChange={onInputChange}
options={model.getOptionsForSelect()}
onChange={(newValue) => {
model.changeValueTo(newValue.value!, newValue.label!);
Expand All @@ -30,6 +39,14 @@ export function VariableValueSelectMulti({ model }: SceneComponentProps<MultiVal
const { value, key } = model.useState();
const arrayValue = isArray(value) ? value : [value];

const onInputChange = model.onSearchChange
? (value: string, meta: InputActionMeta) => {
if (meta.action === 'input-change') {
model.onSearchChange!(value);
}
}
: undefined;

return (
<MultiSelect<VariableValueSingle>
id={key}
Expand All @@ -41,7 +58,7 @@ export function VariableValueSelectMulti({ model }: SceneComponentProps<MultiVal
options={model.getOptionsForSelect()}
closeMenuOnSelect={false}
isClearable={true}
onOpenMenu={() => {}}
onInputChange={onInputChange}
onChange={(newValue) => {
model.changeValueTo(
newValue.map((v) => v.value!),
Expand Down
1 change: 1 addition & 0 deletions packages/scenes/src/variables/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export const AUTO_VARIABLE_VALUE = '$__auto';
* \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3}
*/
export const VARIABLE_REGEX = /\$(\w+)|\[\[(\w+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g;
export const SEARCH_FILTER_VARIABLE = '__searchFilter';
10 changes: 10 additions & 0 deletions packages/scenes/src/variables/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,13 @@ export function isVariableValueEqual(a: VariableValue | null | undefined, b: Var

return isEqual(a, b);
}

export function safeStringifyValue(value: unknown) {
try {
return JSON.stringify(value, null);
} catch (error) {
console.error(error);
}

return '';
}
5 changes: 5 additions & 0 deletions packages/scenes/src/variables/variants/MultiValueVariable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,11 @@ export abstract class MultiValueVariable<TState extends MultiValueVariableState

return options;
}

/**
* Can be used by subclasses to do custom handling of option search based on search input
*/
public onSearchChange?(searchFilter: string): void;
}

export class MultiValueUrlSyncHandler<TState extends MultiValueVariableState = MultiValueVariableState>
Expand Down
76 changes: 54 additions & 22 deletions packages/scenes/src/variables/variants/query/QueryVariable.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { lastValueFrom, of } from 'rxjs';
import React from 'react';

import {
DataQueryRequest,
Expand All @@ -10,6 +11,7 @@ import {
PanelData,
PluginType,
ScopedVars,
StandardVariableQuery,
StandardVariableSupport,
toDataFrame,
toUtc,
Expand All @@ -20,19 +22,28 @@ import { SceneTimeRange } from '../../../core/SceneTimeRange';

import { QueryVariable } from './QueryVariable';
import { QueryRunner, RunnerArgs, setCreateQueryVariableRunnerFactory } from './createQueryVariableRunner';
import { EmbeddedScene } from '../../../components/EmbeddedScene';
import { SceneVariableSet } from '../../sets/SceneVariableSet';
import { VariableValueSelectors } from '../../components/VariableValueSelectors';
import { SceneCanvasText } from '../../../components/SceneCanvasText';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { setRunRequest } from '@grafana/runtime';

const runRequestMock = jest.fn().mockReturnValue(
of<PanelData>({
state: LoadingState.Done,
series: [
toDataFrame({
fields: [{ name: 'text', type: FieldType.string, values: ['A', 'AB', 'C'] }],
fields: [{ name: 'text', type: FieldType.string, values: ['val1', 'val2', 'val11'] }],
}),
],
timeRange: getDefaultTimeRange(),
})
);

setRunRequest(runRequestMock);

const getDataSourceMock = jest.fn();

const fakeDsMock: DataSourceApi = {
Expand Down Expand Up @@ -83,7 +94,9 @@ class FakeQueryRunner implements QueryRunner {
public constructor(private datasource: DataSourceApi, private _runRequest: jest.Mock) {}

public getTarget(variable: QueryVariable) {
return (this.datasource.variables as StandardVariableSupport<DataSourceApi>).toDataQuery(variable.state.query);
return (this.datasource.variables as StandardVariableSupport<DataSourceApi>).toDataQuery(
variable.state.query as StandardVariableQuery
);
}
public runRequest(args: RunnerArgs, request: DataQueryRequest) {
return this._runRequest(
Expand Down Expand Up @@ -111,20 +124,6 @@ describe('QueryVariable', () => {
});
});

describe('When no data source is provided', () => {
it('Should default to empty options and empty value', async () => {
const variable = new QueryVariable({
name: 'test',
});

await lastValueFrom(variable.validateAndUpdate());

expect(variable.state.value).toEqual('');
expect(variable.state.text).toEqual('');
expect(variable.state.options).toEqual([]);
});
});

describe('Issuing variable query', () => {
const originalNow = Date.now;
beforeEach(() => {
Expand All @@ -151,9 +150,9 @@ describe('QueryVariable', () => {
variable.validateAndUpdate().subscribe({
next: () => {
expect(variable.state.options).toEqual([
{ label: 'A', value: 'A' },
{ label: 'AB', value: 'AB' },
{ label: 'C', value: 'C' },
{ label: 'val1', value: 'val1' },
{ label: 'val2', value: 'val2' },
{ label: 'val11', value: 'val11' },
]);
expect(variable.state.loading).toEqual(false);
done();
Expand Down Expand Up @@ -260,15 +259,48 @@ describe('QueryVariable', () => {
name: 'test',
datasource: { uid: 'fake-std', type: 'fake-std' },
query: 'query',
regex: '/^A/',
regex: '/^val1/',
});

await lastValueFrom(variable.validateAndUpdate());

expect(variable.state.options).toEqual([
{ label: 'A', value: 'A' },
{ label: 'AB', value: 'AB' },
{ label: 'val1', value: 'val1' },
{ label: 'val11', value: 'val11' },
]);
});
});

describe('Query with __searchFilter', () => {
beforeEach(() => {
runRequestMock.mockClear();
setCreateQueryVariableRunnerFactory(() => new FakeQueryRunner(fakeDsMock, runRequestMock));
});

it('Should trigger new query and show new options', async () => {
const variable = new QueryVariable({
name: 'server',
datasource: null,
query: 'A.$__searchFilter',
});

const scene = new EmbeddedScene({
$variables: new SceneVariableSet({ variables: [variable] }),
controls: [new VariableValueSelectors({})],
body: new SceneCanvasText({ text: 'hello' }),
});

render(<scene.Component model={scene} />);

const select = await screen.findByRole('combobox');
await userEvent.click(select);
await userEvent.type(select, 'muu!');

// wait for debounce
await new Promise((r) => setTimeout(r, 500));

expect(runRequestMock).toBeCalledTimes(2);
expect(runRequestMock.mock.calls[1][1].scopedVars.__searchFilter.value).toEqual('muu!');
});
});
});
Loading

0 comments on commit 1034177

Please sign in to comment.