mirror of https://github.com/grafana/grafana
Scenes: Add support for Datasource variables (#59147)
parent
0da77201bf
commit
1a6b46e98d
@ -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