mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 23:55:47 -06:00
Scenes: Add support for Datasource variables (#59147)
This commit is contained in:
parent
0da77201bf
commit
1a6b46e98d
@ -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: [] });
|
||||
|
@ -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.*',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
@ -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');
|
||||
}
|
Loading…
Reference in New Issue
Block a user