Scenes: Add support for Datasource variables (#59147)

This commit is contained in:
Ivan Ortega Alba 2022-11-24 13:53:31 +01:00 committed by GitHub
parent 0da77201bf
commit 1a6b46e98d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 369 additions and 27 deletions

View File

@ -1,6 +1,6 @@
import { getDefaultTimeRange, LoadingState } from '@grafana/data';
import { getDefaultTimeRange, LoadingState, ScopedVars } from '@grafana/data';
import { sceneInterpolator } from '../variables/interpolation/sceneInterpolator';
import { CustomFormatterFn, sceneInterpolator } from '../variables/interpolation/sceneInterpolator';
import { SceneVariableSet } from '../variables/sets/SceneVariableSet';
import { SceneVariables } from '../variables/types';
@ -89,13 +89,18 @@ export function getLayout(scene: SceneObject): SceneObject<SceneLayoutState> {
/**
* Interpolates the given string using the current scene object as context. *
*/
export function interpolate(sceneObject: SceneObject, value: string | undefined | null): string {
export function interpolate(
sceneObject: SceneObject,
value: string | undefined | null,
scopedVars?: ScopedVars,
format?: string | CustomFormatterFn
): string {
// Skip interpolation if there are no variable dependencies
if (!value || !sceneObject.variableDependency || sceneObject.variableDependency.getNames().size === 0) {
return value ?? '';
}
return sceneInterpolator(sceneObject, value);
return sceneInterpolator(sceneObject, value, scopedVars, format);
}
export const EmptyVariableSet = new SceneVariableSet({ variables: [] });

View File

@ -7,7 +7,9 @@ import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { VariableValueSelectors } from '../variables/components/VariableValueSelectors';
import { SceneVariableSet } from '../variables/sets/SceneVariableSet';
import { ConstantVariable } from '../variables/variants/ConstantVariable';
import { CustomVariable } from '../variables/variants/CustomVariable';
import { DataSourceVariable } from '../variables/variants/DataSourceVariable';
import { TestVariable } from '../variables/variants/TestVariable';
import { getQueryRunnerWithRandomWalkQuery } from './queries';
@ -43,16 +45,36 @@ export function getVariablesDemo(): Scene {
text: '',
options: [],
}),
new ConstantVariable({
name: 'constant',
value: 'slow',
}),
new CustomVariable({
name: 'Single Custom',
query: 'A : 10,B : 20',
options: [],
}),
new CustomVariable({
name: 'Multi Custom',
query: 'A : 10,B : 20',
isMulti: true,
options: [],
}),
new DataSourceVariable({
name: 'DataSource',
query: 'testdata',
}),
new DataSourceVariable({
name: 'DataSource',
query: 'prometheus',
}),
new DataSourceVariable({
name: 'DataSource multi',
query: 'prometheus',
isMulti: true,
}),
new DataSourceVariable({
name: 'Datasource w/ regex and using $constant',
query: 'prometheus',
regex: '.*$constant.*',
}),
],
}),

View File

@ -9,7 +9,7 @@ import { VariableValue } from '../types';
import { getSceneVariableForScopedVar } from './ScopedVarsVariable';
import { formatRegistry, FormatRegistryID, FormatVariable } from './formatRegistry';
type CustomFormatterFn = (
export type CustomFormatterFn = (
value: unknown,
legacyVariableModel: VariableModel,
legacyDefaultFormatter: CustomFormatterFn

View File

@ -21,26 +21,7 @@ describe('CustomVariable', () => {
});
});
describe('When invalid query is provided', () => {
it('Should default to empty options', async () => {
const variable = new CustomVariable({
name: 'test',
options: [],
value: '',
text: '',
query: 'A - B',
});
// TODO: Be able to triggger the state update to get the options
await lastValueFrom(variable.getValueOptions({}));
expect(variable.state.value).toEqual('');
expect(variable.state.text).toEqual('');
expect(variable.state.options).toEqual([]);
});
});
describe('When valid query is provided', () => {
describe('When query is provided', () => {
it('Should generate correctly the options for only value queries', async () => {
const variable = new CustomVariable({
name: 'test',

View File

@ -0,0 +1,227 @@
import { lastValueFrom } from 'rxjs';
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
import { getMockPlugin } from 'app/features/plugins/__mocks__/pluginMocks';
import { SceneObject } from '../../core/types';
import { CustomFormatterFn } from '../interpolation/sceneInterpolator';
import { DataSourceVariable } from './DataSourceVariable';
function getDataSource(name: string, type: string, isDefault = false): DataSourceInstanceSettings {
return {
id: 1,
uid: 'c8eceabb-0275-4108-8f03-8f74faf4bf6d',
type,
name,
meta: getMockPlugin({ name, id: type }),
jsonData: {},
access: 'proxy',
readOnly: false,
isDefault,
};
}
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getDataSourceSrv: () => ({
getList: () => [
getDataSource('prometheus-mocked', 'prometheus'),
getDataSource('slow-prometheus-mocked', 'prometheus', true),
getDataSource('elastic-mocked', 'elastic'),
],
}),
}));
jest.mock('../../core/sceneGraph', () => {
return {
...jest.requireActual('../../core/sceneGraph'),
sceneGraph: {
interpolate: (
sceneObject: SceneObject,
value: string | undefined | null,
scopedVars?: ScopedVars,
format?: string | CustomFormatterFn
) => {
return value?.replace('$variable-1', 'slow');
},
},
};
});
describe('DataSourceVariable', () => {
describe('When empty query is provided', () => {
it('Should default to empty options and empty value', async () => {
const variable = new DataSourceVariable({
name: 'test',
options: [],
value: '',
text: '',
query: '',
});
await lastValueFrom(variable.validateAndUpdate());
expect(variable.state.value).toEqual('');
expect(variable.state.text).toEqual('');
expect(variable.state.options).toEqual([]);
});
});
describe('When query is provided', () => {
it('Should default to non datasources found options for invalid query', async () => {
const variable = new DataSourceVariable({
name: 'test',
options: [],
value: '',
text: '',
query: 'non-existant-datasource',
});
await lastValueFrom(variable.validateAndUpdate());
expect(variable.state.value).toEqual('');
expect(variable.state.text).toEqual('');
expect(variable.state.options).toEqual([
{
label: 'No data sources found',
value: '',
},
]);
});
it('Should default to first item datasource when options available', async () => {
const variable = new DataSourceVariable({
name: 'test',
options: [],
value: '',
text: '',
query: 'prometheus',
});
await lastValueFrom(variable.validateAndUpdate());
expect(variable.state.value).toEqual('prometheus-mocked');
expect(variable.state.text).toEqual('prometheus-mocked');
expect(variable.state.options).toEqual([
{
label: 'prometheus-mocked',
value: 'prometheus-mocked',
},
{
label: 'slow-prometheus-mocked',
value: 'slow-prometheus-mocked',
},
{
label: 'default',
value: 'default',
},
]);
});
it('Should generate correctly the options including only datasources with the queried type', async () => {
const variable = new DataSourceVariable({
name: 'test',
options: [],
value: '',
text: '',
query: 'prometheus',
});
await lastValueFrom(variable.validateAndUpdate());
expect(variable.state.value).toEqual('prometheus-mocked');
expect(variable.state.text).toEqual('prometheus-mocked');
expect(variable.state.options).toEqual([
{ label: 'prometheus-mocked', value: 'prometheus-mocked' },
{ label: 'slow-prometheus-mocked', value: 'slow-prometheus-mocked' },
{ label: 'default', value: 'default' },
]);
});
});
describe('When regex is provided', () => {
it('Should generate correctly the options including only datasources with matching', async () => {
const variable = new DataSourceVariable({
name: 'test',
options: [],
value: '',
text: '',
query: 'prometheus',
regex: 'slow.*',
});
await lastValueFrom(variable.validateAndUpdate());
expect(variable.state.value).toEqual('slow-prometheus-mocked');
expect(variable.state.text).toEqual('slow-prometheus-mocked');
expect(variable.state.options).toEqual([{ label: 'slow-prometheus-mocked', value: 'slow-prometheus-mocked' }]);
});
it('Should generate correctly the options after interpolating variables', async () => {
const variable = new DataSourceVariable({
name: 'test',
options: [],
value: '',
text: '',
query: 'prometheus',
regex: '$variable-1.*',
});
await lastValueFrom(variable.validateAndUpdate());
expect(variable.state.value).toEqual('slow-prometheus-mocked');
expect(variable.state.text).toEqual('slow-prometheus-mocked');
expect(variable.state.options).toEqual([{ label: 'slow-prometheus-mocked', value: 'slow-prometheus-mocked' }]);
});
});
describe('When value is provided', () => {
it('Should keep current value if current value is valid', async () => {
const variable = new DataSourceVariable({
name: 'test',
options: [],
query: 'prometheus',
value: 'slow-prometheus-mocked',
text: 'slow-prometheus-mocked',
});
await lastValueFrom(variable.validateAndUpdate());
expect(variable.state.value).toBe('slow-prometheus-mocked');
expect(variable.state.text).toBe('slow-prometheus-mocked');
});
it('Should maintain the valid values when multiple selected', async () => {
const variable = new DataSourceVariable({
name: 'test',
options: [],
isMulti: true,
query: 'prometheus',
value: ['prometheus-mocked', 'slow-prometheus-mocked', 'elastic-mocked'],
text: ['prometheus-mocked', 'slow-prometheus-mocked', 'elastic-mocked'],
});
await lastValueFrom(variable.validateAndUpdate());
expect(variable.state.value).toEqual(['prometheus-mocked', 'slow-prometheus-mocked']);
expect(variable.state.text).toEqual(['prometheus-mocked', 'slow-prometheus-mocked']);
});
it('Should pick first option if none of the current values are valid', async () => {
const variable = new DataSourceVariable({
name: 'test',
options: [],
isMulti: true,
query: 'elastic',
value: ['prometheus-mocked', 'slow-prometheus-mocked'],
text: ['prometheus-mocked', 'slow-prometheus-mocked'],
});
await lastValueFrom(variable.validateAndUpdate());
expect(variable.state.value).toEqual(['elastic-mocked']);
expect(variable.state.text).toEqual(['elastic-mocked']);
});
});
});

View File

@ -0,0 +1,107 @@
import React from 'react';
import { Observable, of } from 'rxjs';
import { stringToJsRegex, DataSourceInstanceSettings } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { sceneGraph } from '../../core/sceneGraph';
import { SceneComponentProps } from '../../core/types';
import { VariableDependencyConfig } from '../VariableDependencyConfig';
import { VariableValueSelect } from '../components/VariableValueSelect';
import { VariableValueOption } from '../types';
import { MultiValueVariable, MultiValueVariableState, VariableGetOptionsArgs } from './MultiValueVariable';
export interface DataSourceVariableState extends MultiValueVariableState {
query: string;
regex: string;
}
export class DataSourceVariable extends MultiValueVariable<DataSourceVariableState> {
protected _variableDependency = new VariableDependencyConfig(this, {
statePaths: ['regex'],
});
public constructor(initialState: Partial<DataSourceVariableState>) {
super({
value: '',
text: '',
options: [],
name: '',
regex: '',
query: '',
...initialState,
});
}
public getValueOptions(args: VariableGetOptionsArgs): Observable<VariableValueOption[]> {
if (!this.state.query) {
return of([]);
}
const dataSourceTypes = this.getDataSourceTypes();
let regex;
if (this.state.regex) {
const interpolated = sceneGraph.interpolate(this, this.state.regex, undefined, 'regex');
regex = stringToJsRegex(interpolated);
}
const options: VariableValueOption[] = [];
for (let i = 0; i < dataSourceTypes.length; i++) {
const source = dataSourceTypes[i];
// must match on type
if (source.meta.id !== this.state.query) {
continue;
}
if (isValid(source, regex)) {
options.push({ label: source.name, value: source.name });
}
if (isDefault(source, regex)) {
options.push({ label: 'default', value: 'default' });
}
}
if (options.length === 0) {
options.push({ label: 'No data sources found', value: '' });
}
// TODO: Add support for include All
// if (instanceState.includeAll) {
// options.unshift({ label: ALL_VARIABLE_TEXT, value: ALL_VARIABLE_VALUE });
//}
return of(options);
}
private getDataSourceTypes(): DataSourceInstanceSettings[] {
return getDataSourceSrv().getList({ metrics: true, variables: false });
}
public static Component = ({ model }: SceneComponentProps<MultiValueVariable>) => {
return <VariableValueSelect model={model} />;
};
}
function isValid(source: DataSourceInstanceSettings, regex?: RegExp) {
if (!regex) {
return true;
}
return regex.exec(source.name);
}
function isDefault(source: DataSourceInstanceSettings, regex?: RegExp) {
if (!source.isDefault) {
return false;
}
if (!regex) {
return true;
}
return regex.exec('default');
}