mirror of
https://github.com/grafana/grafana.git
synced 2025-02-14 01:23:32 -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 { SceneVariableSet } from '../variables/sets/SceneVariableSet';
|
||||||
import { SceneVariables } from '../variables/types';
|
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. *
|
* 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
|
// Skip interpolation if there are no variable dependencies
|
||||||
if (!value || !sceneObject.variableDependency || sceneObject.variableDependency.getNames().size === 0) {
|
if (!value || !sceneObject.variableDependency || sceneObject.variableDependency.getNames().size === 0) {
|
||||||
return value ?? '';
|
return value ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return sceneInterpolator(sceneObject, value);
|
return sceneInterpolator(sceneObject, value, scopedVars, format);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmptyVariableSet = new SceneVariableSet({ variables: [] });
|
export const EmptyVariableSet = new SceneVariableSet({ variables: [] });
|
||||||
|
@ -7,7 +7,9 @@ import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
|
|||||||
import { SceneTimeRange } from '../core/SceneTimeRange';
|
import { SceneTimeRange } from '../core/SceneTimeRange';
|
||||||
import { VariableValueSelectors } from '../variables/components/VariableValueSelectors';
|
import { VariableValueSelectors } from '../variables/components/VariableValueSelectors';
|
||||||
import { SceneVariableSet } from '../variables/sets/SceneVariableSet';
|
import { SceneVariableSet } from '../variables/sets/SceneVariableSet';
|
||||||
|
import { ConstantVariable } from '../variables/variants/ConstantVariable';
|
||||||
import { CustomVariable } from '../variables/variants/CustomVariable';
|
import { CustomVariable } from '../variables/variants/CustomVariable';
|
||||||
|
import { DataSourceVariable } from '../variables/variants/DataSourceVariable';
|
||||||
import { TestVariable } from '../variables/variants/TestVariable';
|
import { TestVariable } from '../variables/variants/TestVariable';
|
||||||
|
|
||||||
import { getQueryRunnerWithRandomWalkQuery } from './queries';
|
import { getQueryRunnerWithRandomWalkQuery } from './queries';
|
||||||
@ -43,16 +45,36 @@ export function getVariablesDemo(): Scene {
|
|||||||
text: '',
|
text: '',
|
||||||
options: [],
|
options: [],
|
||||||
}),
|
}),
|
||||||
|
new ConstantVariable({
|
||||||
|
name: 'constant',
|
||||||
|
value: 'slow',
|
||||||
|
}),
|
||||||
new CustomVariable({
|
new CustomVariable({
|
||||||
name: 'Single Custom',
|
name: 'Single Custom',
|
||||||
query: 'A : 10,B : 20',
|
query: 'A : 10,B : 20',
|
||||||
options: [],
|
|
||||||
}),
|
}),
|
||||||
new CustomVariable({
|
new CustomVariable({
|
||||||
name: 'Multi Custom',
|
name: 'Multi Custom',
|
||||||
query: 'A : 10,B : 20',
|
query: 'A : 10,B : 20',
|
||||||
isMulti: true,
|
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 { getSceneVariableForScopedVar } from './ScopedVarsVariable';
|
||||||
import { formatRegistry, FormatRegistryID, FormatVariable } from './formatRegistry';
|
import { formatRegistry, FormatRegistryID, FormatVariable } from './formatRegistry';
|
||||||
|
|
||||||
type CustomFormatterFn = (
|
export type CustomFormatterFn = (
|
||||||
value: unknown,
|
value: unknown,
|
||||||
legacyVariableModel: VariableModel,
|
legacyVariableModel: VariableModel,
|
||||||
legacyDefaultFormatter: CustomFormatterFn
|
legacyDefaultFormatter: CustomFormatterFn
|
||||||
|
@ -21,26 +21,7 @@ describe('CustomVariable', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When invalid query is provided', () => {
|
describe('When 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', () => {
|
|
||||||
it('Should generate correctly the options for only value queries', async () => {
|
it('Should generate correctly the options for only value queries', async () => {
|
||||||
const variable = new CustomVariable({
|
const variable = new CustomVariable({
|
||||||
name: 'test',
|
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