mirror of https://github.com/grafana/grafana
Scenes: Add query variable support (#59553)
* WIP first attempt to query variable * regex issue repro demo * Refresh variable on time range change if refresh specified * Instantiate variable runner when updating query variable options * Simplify runners getTarget interface * Fix issue with variable ot being updated correctly after other variable changed * Add templateSrv.replace compatibility with query variable * QueryVariable: use datasource variable as source * use proper format * Make sure variables set is correctly updated when query variable errors * Do not destruct scopedVars when using sceneGraph.interpolate in templateSrv * Add support for Legacy variables (metricFindQuery) * Review * Fix lint * Test: Add unit for datasource by variable * test: Add unit for datasource as var * query: delegate interpolation to datasourceSrv * Cleanup Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>home-dashboard-tweak
parent
712e23ac50
commit
1758ddd457
@ -0,0 +1,71 @@ |
||||
import { VariableRefresh } from '@grafana/data'; |
||||
|
||||
import { Scene, EmbeddedScene } from '../components/Scene'; |
||||
import { SceneCanvasText } from '../components/SceneCanvasText'; |
||||
import { SceneSubMenu } from '../components/SceneSubMenu'; |
||||
import { SceneTimePicker } from '../components/SceneTimePicker'; |
||||
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout'; |
||||
import { SceneTimeRange } from '../core/SceneTimeRange'; |
||||
import { VariableValueSelectors } from '../variables/components/VariableValueSelectors'; |
||||
import { SceneVariableSet } from '../variables/sets/SceneVariableSet'; |
||||
import { CustomVariable } from '../variables/variants/CustomVariable'; |
||||
import { DataSourceVariable } from '../variables/variants/DataSourceVariable'; |
||||
import { QueryVariable } from '../variables/variants/query/QueryVariable'; |
||||
|
||||
export function getQueryVariableDemo(standalone: boolean): Scene { |
||||
const state = { |
||||
title: 'Query variable', |
||||
$variables: new SceneVariableSet({ |
||||
variables: [ |
||||
new CustomVariable({ |
||||
name: 'metric', |
||||
query: 'job : job, instance : instance', |
||||
}), |
||||
new DataSourceVariable({ |
||||
name: 'datasource', |
||||
query: 'prometheus', |
||||
}), |
||||
new QueryVariable({ |
||||
name: 'instance (using datasource variable)', |
||||
refresh: VariableRefresh.onTimeRangeChanged, |
||||
query: { query: 'label_values(go_gc_duration_seconds, ${metric})' }, |
||||
datasource: '${datasource}', |
||||
}), |
||||
new QueryVariable({ |
||||
name: 'label values (on time range refresh)', |
||||
refresh: VariableRefresh.onTimeRangeChanged, |
||||
query: { query: 'label_values(go_gc_duration_seconds, ${metric})' }, |
||||
datasource: { uid: 'gdev-prometheus', type: 'prometheus' }, |
||||
}), |
||||
new QueryVariable({ |
||||
name: 'legacy (graphite)', |
||||
refresh: VariableRefresh.onTimeRangeChanged, |
||||
query: { queryType: 'Default', target: 'stats.response.*' }, |
||||
datasource: { uid: 'gdev-graphite', type: 'graphite' }, |
||||
}), |
||||
], |
||||
}), |
||||
layout: new SceneFlexLayout({ |
||||
direction: 'row', |
||||
children: [ |
||||
new SceneFlexLayout({ |
||||
children: [ |
||||
new SceneCanvasText({ |
||||
size: { width: '40%' }, |
||||
text: 'metric: ${metric}', |
||||
fontSize: 20, |
||||
align: 'center', |
||||
}), |
||||
], |
||||
}), |
||||
], |
||||
}), |
||||
$timeRange: new SceneTimeRange(), |
||||
actions: [new SceneTimePicker({})], |
||||
subMenu: new SceneSubMenu({ |
||||
children: [new VariableValueSelectors({})], |
||||
}), |
||||
}; |
||||
|
||||
return standalone ? new Scene(state) : new EmbeddedScene(state); |
||||
} |
@ -0,0 +1,310 @@ |
||||
import { lastValueFrom, of } from 'rxjs'; |
||||
|
||||
import { |
||||
DataQueryRequest, |
||||
DataSourceApi, |
||||
DataSourceRef, |
||||
FieldType, |
||||
getDefaultTimeRange, |
||||
LoadingState, |
||||
PanelData, |
||||
PluginType, |
||||
StandardVariableSupport, |
||||
toDataFrame, |
||||
toUtc, |
||||
VariableRefresh, |
||||
VariableSupportType, |
||||
} from '@grafana/data'; |
||||
import { SceneFlexLayout } from 'app/features/scenes/components'; |
||||
import { SceneTimeRange } from 'app/features/scenes/core/SceneTimeRange'; |
||||
|
||||
import { SceneVariableSet } from '../../sets/SceneVariableSet'; |
||||
|
||||
import { QueryVariable } from './QueryVariable'; |
||||
import { QueryRunner, RunnerArgs, setCreateQueryVariableRunnerFactory } from './createQueryVariableRunner'; |
||||
|
||||
const runRequestMock = jest.fn().mockReturnValue( |
||||
of<PanelData>({ |
||||
state: LoadingState.Done, |
||||
series: [ |
||||
toDataFrame({ |
||||
fields: [{ name: 'text', type: FieldType.string, values: ['A', 'AB', 'C'] }], |
||||
}), |
||||
], |
||||
timeRange: getDefaultTimeRange(), |
||||
}) |
||||
); |
||||
|
||||
const fakeDsMock: DataSourceApi = { |
||||
name: 'fake-std', |
||||
type: 'fake-std', |
||||
getRef: () => ({ type: 'fake-std', uid: 'fake-std' }), |
||||
query: () => |
||||
Promise.resolve({ |
||||
data: [], |
||||
}), |
||||
testDatasource: () => Promise.resolve({ status: 'success' }), |
||||
meta: { |
||||
id: 'fake-std', |
||||
type: PluginType.datasource, |
||||
module: 'fake-std', |
||||
baseUrl: '', |
||||
name: 'fake-std', |
||||
info: { |
||||
author: { name: '' }, |
||||
description: '', |
||||
links: [], |
||||
logos: { large: '', small: '' }, |
||||
updated: '', |
||||
version: '', |
||||
screenshots: [], |
||||
}, |
||||
}, |
||||
// Standard variable support
|
||||
variables: { |
||||
getType: () => VariableSupportType.Standard, |
||||
toDataQuery: (q) => ({ ...q, refId: 'FakeDataSource-refId' }), |
||||
}, |
||||
id: 1, |
||||
uid: 'fake-std', |
||||
}; |
||||
|
||||
jest.mock('@grafana/runtime', () => ({ |
||||
...jest.requireActual('@grafana/runtime'), |
||||
getDataSourceSrv: () => ({ |
||||
get: (ds: DataSourceRef): Promise<DataSourceApi> => { |
||||
return Promise.resolve(fakeDsMock); |
||||
}, |
||||
}), |
||||
})); |
||||
|
||||
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); |
||||
} |
||||
public runRequest(args: RunnerArgs, request: DataQueryRequest) { |
||||
return this._runRequest( |
||||
this.datasource, |
||||
request, |
||||
(this.datasource.variables as StandardVariableSupport<DataSourceApi>).query |
||||
); |
||||
} |
||||
} |
||||
|
||||
describe('QueryVariable', () => { |
||||
describe('When empty query is provided', () => { |
||||
it('Should default to empty options and empty value', async () => { |
||||
const variable = new QueryVariable({ |
||||
name: 'test', |
||||
datasource: { uid: 'fake', type: 'fake' }, |
||||
query: '', |
||||
}); |
||||
|
||||
await lastValueFrom(variable.validateAndUpdate()); |
||||
|
||||
expect(variable.state.value).toEqual(''); |
||||
expect(variable.state.text).toEqual(''); |
||||
expect(variable.state.options).toEqual([]); |
||||
}); |
||||
}); |
||||
|
||||
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(() => { |
||||
setCreateQueryVariableRunnerFactory(() => new FakeQueryRunner(fakeDsMock, runRequestMock)); |
||||
}); |
||||
|
||||
beforeEach(() => { |
||||
Date.now = jest.fn(() => 60000); |
||||
}); |
||||
|
||||
afterEach(() => { |
||||
Date.now = originalNow; |
||||
runRequestMock.mockClear(); |
||||
}); |
||||
|
||||
it('Should resolve variable options via provided runner', async () => { |
||||
const variable = new QueryVariable({ |
||||
name: 'test', |
||||
datasource: { uid: 'fake-std', type: 'fake-std' }, |
||||
query: 'query', |
||||
}); |
||||
|
||||
await lastValueFrom(variable.validateAndUpdate()); |
||||
|
||||
expect(variable.state.options).toEqual([ |
||||
{ label: 'A', value: 'A' }, |
||||
{ label: 'AB', value: 'AB' }, |
||||
{ label: 'C', value: 'C' }, |
||||
]); |
||||
}); |
||||
|
||||
it('Should pass variable scene object via request scoped vars', async () => { |
||||
const variable = new QueryVariable({ |
||||
name: 'test', |
||||
datasource: { uid: 'fake-std', type: 'fake-std' }, |
||||
query: 'query', |
||||
}); |
||||
|
||||
await lastValueFrom(variable.validateAndUpdate()); |
||||
const call = runRequestMock.mock.calls[0]; |
||||
expect(call[1].scopedVars.__sceneObject).toEqual({ value: variable, text: '__sceneObject' }); |
||||
}); |
||||
|
||||
describe('when refresh on dashboard load set', () => { |
||||
it('Should issue variable query with default time range', async () => { |
||||
const variable = new QueryVariable({ |
||||
name: 'test', |
||||
datasource: { uid: 'fake-std', type: 'fake-std' }, |
||||
query: 'query', |
||||
}); |
||||
|
||||
await lastValueFrom(variable.validateAndUpdate()); |
||||
|
||||
expect(runRequestMock).toBeCalledTimes(1); |
||||
const call = runRequestMock.mock.calls[0]; |
||||
expect(call[1].range).toEqual(getDefaultTimeRange()); |
||||
}); |
||||
|
||||
it('Should not issue variable query when the closest time range changes if refresh on dahboard load is set', async () => { |
||||
const timeRange = new SceneTimeRange({ from: 'now-1h', to: 'now' }); |
||||
|
||||
const variable = new QueryVariable({ |
||||
name: 'test', |
||||
datasource: { uid: 'fake-std', type: 'fake-std' }, |
||||
query: 'query', |
||||
refresh: VariableRefresh.onDashboardLoad, |
||||
$timeRange: timeRange, |
||||
}); |
||||
|
||||
variable.activate(); |
||||
|
||||
await lastValueFrom(variable.validateAndUpdate()); |
||||
|
||||
expect(runRequestMock).toBeCalledTimes(1); |
||||
const call1 = runRequestMock.mock.calls[0]; |
||||
|
||||
// Uses default time range
|
||||
expect(call1[1].range.raw).toEqual({ |
||||
from: 'now-6h', |
||||
to: 'now', |
||||
}); |
||||
|
||||
timeRange.onTimeRangeChange({ |
||||
from: toUtc('2020-01-01'), |
||||
to: toUtc('2020-01-02'), |
||||
raw: { from: toUtc('2020-01-01'), to: toUtc('2020-01-02') }, |
||||
}); |
||||
|
||||
await Promise.resolve(); |
||||
|
||||
expect(runRequestMock).toBeCalledTimes(1); |
||||
}); |
||||
}); |
||||
|
||||
describe('when refresh on time range change set', () => { |
||||
it('Should issue variable query with closes time range if refresh on time range change set', async () => { |
||||
const variable = new QueryVariable({ |
||||
name: 'test', |
||||
datasource: { uid: 'fake-std', type: 'fake-std' }, |
||||
query: 'query', |
||||
refresh: VariableRefresh.onTimeRangeChanged, |
||||
}); |
||||
|
||||
// @ts-expect-error
|
||||
const scene = new SceneFlexLayout({ |
||||
$timeRange: new SceneTimeRange({ from: 'now-1h', to: 'now' }), |
||||
$variables: new SceneVariableSet({ |
||||
variables: [variable], |
||||
}), |
||||
children: [], |
||||
}); |
||||
|
||||
await lastValueFrom(variable.validateAndUpdate()); |
||||
|
||||
expect(runRequestMock).toBeCalledTimes(1); |
||||
const call = runRequestMock.mock.calls[0]; |
||||
|
||||
expect(call[1].range.raw).toEqual({ |
||||
from: 'now-1h', |
||||
to: 'now', |
||||
}); |
||||
}); |
||||
|
||||
it('Should issue variable query when time range changes if refresh on time range change is set', async () => { |
||||
const timeRange = new SceneTimeRange({ from: 'now-1h', to: 'now' }); |
||||
const variable = new QueryVariable({ |
||||
name: 'test', |
||||
datasource: { uid: 'fake-std', type: 'fake-std' }, |
||||
query: 'query', |
||||
refresh: VariableRefresh.onTimeRangeChanged, |
||||
$timeRange: timeRange, |
||||
}); |
||||
|
||||
variable.activate(); |
||||
|
||||
await lastValueFrom(variable.validateAndUpdate()); |
||||
|
||||
expect(runRequestMock).toBeCalledTimes(1); |
||||
const call1 = runRequestMock.mock.calls[0]; |
||||
expect(call1[1].range.raw).toEqual({ |
||||
from: 'now-1h', |
||||
to: 'now', |
||||
}); |
||||
|
||||
timeRange.onTimeRangeChange({ |
||||
from: toUtc('2020-01-01'), |
||||
to: toUtc('2020-01-02'), |
||||
raw: { from: toUtc('2020-01-01'), to: toUtc('2020-01-02') }, |
||||
}); |
||||
|
||||
await new Promise((r) => setTimeout(r, 1)); |
||||
|
||||
expect(runRequestMock).toBeCalledTimes(2); |
||||
const call2 = runRequestMock.mock.calls[1]; |
||||
expect(call2[1].range.raw).toEqual({ |
||||
from: '2020-01-01T00:00:00.000Z', |
||||
to: '2020-01-02T00:00:00.000Z', |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('When regex provided', () => { |
||||
beforeEach(() => { |
||||
setCreateQueryVariableRunnerFactory(() => new FakeQueryRunner(fakeDsMock, runRequestMock)); |
||||
}); |
||||
|
||||
it('should return options that match regex', async () => { |
||||
const variable = new QueryVariable({ |
||||
name: 'test', |
||||
datasource: { uid: 'fake-std', type: 'fake-std' }, |
||||
query: 'query', |
||||
regex: '/^A/', |
||||
}); |
||||
|
||||
await lastValueFrom(variable.validateAndUpdate()); |
||||
|
||||
expect(variable.state.options).toEqual([ |
||||
{ label: 'A', value: 'A' }, |
||||
{ label: 'AB', value: 'AB' }, |
||||
]); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,166 @@ |
||||
import React from 'react'; |
||||
import { Observable, Subject, of, Unsubscribable, filter, take, mergeMap, catchError, throwError, from } from 'rxjs'; |
||||
import { v4 as uuidv4 } from 'uuid'; |
||||
|
||||
import { |
||||
CoreApp, |
||||
DataQuery, |
||||
DataQueryRequest, |
||||
DataSourceApi, |
||||
DataSourceRef, |
||||
getDefaultTimeRange, |
||||
LoadingState, |
||||
PanelData, |
||||
ScopedVars, |
||||
VariableRefresh, |
||||
VariableSort, |
||||
} from '@grafana/data'; |
||||
import { getDataSourceSrv } from '@grafana/runtime'; |
||||
import { toMetricFindValues } from 'app/features/variables/query/operators'; |
||||
|
||||
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'; |
||||
|
||||
import { createQueryVariableRunner } from './createQueryVariableRunner'; |
||||
import { metricNamesToVariableValues } from './utils'; |
||||
|
||||
export interface QueryVariableState extends MultiValueVariableState { |
||||
type: 'query'; |
||||
datasource: DataSourceRef | string | null; |
||||
query: any; |
||||
regex: string; |
||||
refresh: VariableRefresh; |
||||
sort: VariableSort; |
||||
} |
||||
|
||||
export class QueryVariable extends MultiValueVariable<QueryVariableState> { |
||||
private updateSubscription?: Unsubscribable; |
||||
private dataSourceSubject?: Subject<DataSourceApi>; |
||||
|
||||
protected _variableDependency = new VariableDependencyConfig(this, { |
||||
statePaths: ['regex', 'query', 'datasource'], |
||||
}); |
||||
|
||||
public constructor(initialState: Partial<QueryVariableState>) { |
||||
super({ |
||||
type: 'query', |
||||
name: '', |
||||
value: '', |
||||
text: '', |
||||
query: '', |
||||
options: [], |
||||
datasource: null, |
||||
regex: '', |
||||
refresh: VariableRefresh.onDashboardLoad, |
||||
sort: VariableSort.alphabeticalAsc, |
||||
...initialState, |
||||
}); |
||||
} |
||||
|
||||
public activate(): void { |
||||
super.activate(); |
||||
const timeRange = sceneGraph.getTimeRange(this); |
||||
|
||||
if (this.state.refresh === VariableRefresh.onTimeRangeChanged) { |
||||
this._subs.add( |
||||
timeRange.subscribeToState({ |
||||
next: () => { |
||||
this.updateSubscription = this.validateAndUpdate().subscribe(); |
||||
}, |
||||
}) |
||||
); |
||||
} |
||||
} |
||||
|
||||
public deactivate(): void { |
||||
super.deactivate(); |
||||
if (this.updateSubscription) { |
||||
this.updateSubscription.unsubscribe(); |
||||
} |
||||
|
||||
if (this.dataSourceSubject) { |
||||
this.dataSourceSubject.unsubscribe(); |
||||
} |
||||
} |
||||
|
||||
public getValueOptions(args: VariableGetOptionsArgs): Observable<VariableValueOption[]> { |
||||
if (this.state.query === '' || !this.state.datasource) { |
||||
return of([]); |
||||
} |
||||
|
||||
return from(this.getDataSource()).pipe( |
||||
mergeMap((ds) => { |
||||
const runner = createQueryVariableRunner(ds); |
||||
const target = runner.getTarget(this); |
||||
const request = this.getRequest(target); |
||||
return runner.runRequest({ variable: this }, request).pipe( |
||||
filter((data) => data.state === LoadingState.Done || data.state === LoadingState.Error), // we only care about done or error for now
|
||||
take(1), // take the first result, using first caused a bug where it in some situations throw an uncaught error because of no results had been received yet
|
||||
mergeMap((data: PanelData) => { |
||||
if (data.state === LoadingState.Error) { |
||||
return throwError(() => data.error); |
||||
} |
||||
return of(data); |
||||
}), |
||||
toMetricFindValues(), |
||||
mergeMap((values) => { |
||||
let regex = ''; |
||||
if (this.state.regex) { |
||||
regex = sceneGraph.interpolate(this, this.state.regex, undefined, 'regex'); |
||||
} |
||||
return of(metricNamesToVariableValues(regex, this.state.sort, values)); |
||||
}), |
||||
catchError((error) => { |
||||
if (error.cancelled) { |
||||
return of([]); |
||||
} |
||||
return throwError(() => error); |
||||
}) |
||||
); |
||||
}) |
||||
); |
||||
} |
||||
|
||||
private async getDataSource(): Promise<DataSourceApi> { |
||||
return getDataSourceSrv().get(this.state.datasource ?? '', { |
||||
__sceneObject: { text: '__sceneObject', value: this }, |
||||
}); |
||||
} |
||||
|
||||
private getRequest(target: DataQuery) { |
||||
// TODO: add support for search filter
|
||||
// const { searchFilter } = this.state.searchFilter;
|
||||
// const searchFilterScope = { searchFilter: { text: searchFilter, value: searchFilter } };
|
||||
// const searchFilterAsVars = searchFilter ? searchFilterScope : {};
|
||||
const scopedVars: ScopedVars = { |
||||
// ...searchFilterAsVars,
|
||||
__sceneObject: { text: '__sceneObject', value: this }, |
||||
}; |
||||
|
||||
const range = |
||||
this.state.refresh === VariableRefresh.onTimeRangeChanged |
||||
? sceneGraph.getTimeRange(this).state.value |
||||
: getDefaultTimeRange(); |
||||
|
||||
const request: DataQueryRequest = { |
||||
app: CoreApp.Dashboard, |
||||
requestId: uuidv4(), |
||||
timezone: '', |
||||
range, |
||||
interval: '', |
||||
intervalMs: 0, |
||||
targets: [target], |
||||
scopedVars, |
||||
startTime: Date.now(), |
||||
}; |
||||
return request; |
||||
} |
||||
|
||||
public static Component = ({ model }: SceneComponentProps<MultiValueVariable>) => { |
||||
return <VariableValueSelect model={model} />; |
||||
}; |
||||
} |
@ -0,0 +1,115 @@ |
||||
import { from, mergeMap, Observable, of } from 'rxjs'; |
||||
|
||||
import { |
||||
DataQuery, |
||||
DataQueryRequest, |
||||
DataSourceApi, |
||||
getDefaultTimeRange, |
||||
LoadingState, |
||||
PanelData, |
||||
} from '@grafana/data'; |
||||
import { runRequest } from 'app/features/query/state/runRequest'; |
||||
import { hasLegacyVariableSupport, hasStandardVariableSupport } from 'app/features/variables/guard'; |
||||
|
||||
import { QueryVariable } from './QueryVariable'; |
||||
|
||||
export interface RunnerArgs { |
||||
searchFilter?: string; |
||||
variable: QueryVariable; |
||||
} |
||||
|
||||
export interface QueryRunner { |
||||
getTarget: (variable: QueryVariable) => DataQuery; |
||||
runRequest: (args: RunnerArgs, request: DataQueryRequest) => Observable<PanelData>; |
||||
} |
||||
|
||||
class StandardQueryRunner implements QueryRunner { |
||||
public constructor(private datasource: DataSourceApi, private _runRequest = runRequest) {} |
||||
|
||||
public getTarget(variable: QueryVariable) { |
||||
if (hasStandardVariableSupport(this.datasource)) { |
||||
return this.datasource.variables.toDataQuery(variable.state.query); |
||||
} |
||||
|
||||
throw new Error("Couldn't create a target with supplied arguments."); |
||||
} |
||||
|
||||
public runRequest(_: RunnerArgs, request: DataQueryRequest) { |
||||
if (!hasStandardVariableSupport(this.datasource)) { |
||||
return getEmptyMetricFindValueObservable(); |
||||
} |
||||
|
||||
if (!this.datasource.variables.query) { |
||||
return this._runRequest(this.datasource, request); |
||||
} |
||||
|
||||
return this._runRequest(this.datasource, request, this.datasource.variables.query); |
||||
} |
||||
} |
||||
|
||||
class LegacyQueryRunner implements QueryRunner { |
||||
public constructor(private datasource: DataSourceApi) {} |
||||
|
||||
public getTarget(variable: QueryVariable) { |
||||
if (hasLegacyVariableSupport(this.datasource)) { |
||||
return variable.state.query; |
||||
} |
||||
|
||||
throw new Error("Couldn't create a target with supplied arguments."); |
||||
} |
||||
|
||||
public runRequest({ variable }: RunnerArgs, request: DataQueryRequest) { |
||||
if (!hasLegacyVariableSupport(this.datasource)) { |
||||
return getEmptyMetricFindValueObservable(); |
||||
} |
||||
|
||||
return from( |
||||
this.datasource.metricFindQuery(variable.state.query, { |
||||
...request, |
||||
// variable is used by SQL common data source
|
||||
variable: { |
||||
name: variable.state.name, |
||||
type: variable.state.type, |
||||
}, |
||||
// TODO: add support for search filter
|
||||
// searchFilter
|
||||
}) |
||||
).pipe( |
||||
mergeMap((values) => { |
||||
if (!values || !values.length) { |
||||
return getEmptyMetricFindValueObservable(); |
||||
} |
||||
|
||||
const series: any = values; |
||||
return of({ series, state: LoadingState.Done, timeRange: request.range }); |
||||
}) |
||||
); |
||||
} |
||||
} |
||||
|
||||
function getEmptyMetricFindValueObservable(): Observable<PanelData> { |
||||
return of({ state: LoadingState.Done, series: [], timeRange: getDefaultTimeRange() }); |
||||
} |
||||
|
||||
function createQueryVariableRunnerFactory(datasource: DataSourceApi): QueryRunner { |
||||
if (hasStandardVariableSupport(datasource)) { |
||||
return new StandardQueryRunner(datasource, runRequest); |
||||
} |
||||
|
||||
if (hasLegacyVariableSupport(datasource)) { |
||||
return new LegacyQueryRunner(datasource); |
||||
} |
||||
|
||||
// TODO: add support for legacy, cutom and datasource query runners
|
||||
|
||||
throw new Error(`Couldn't create a query runner for datasource ${datasource.type}`); |
||||
} |
||||
|
||||
export let createQueryVariableRunner = createQueryVariableRunnerFactory; |
||||
|
||||
/** |
||||
* Use only in tests |
||||
*/ |
||||
export function setCreateQueryVariableRunnerFactory(fn: (datasource: DataSourceApi) => QueryRunner) { |
||||
createQueryVariableRunner = fn; |
||||
} |
@ -0,0 +1,111 @@ |
||||
import { isNumber, sortBy, toLower, uniqBy } from 'lodash'; |
||||
|
||||
import { stringToJsRegex, VariableSort } from '@grafana/data'; |
||||
|
||||
import { VariableValueOption } from '../../types'; |
||||
|
||||
export const metricNamesToVariableValues = (variableRegEx: string, sort: VariableSort, metricNames: any[]) => { |
||||
let regex; |
||||
let options: VariableValueOption[] = []; |
||||
|
||||
if (variableRegEx) { |
||||
regex = stringToJsRegex(variableRegEx); |
||||
} |
||||
|
||||
for (let i = 0; i < metricNames.length; i++) { |
||||
const item = metricNames[i]; |
||||
let text = item.text === undefined || item.text === null ? item.value : item.text; |
||||
let value = item.value === undefined || item.value === null ? item.text : item.value; |
||||
|
||||
if (isNumber(value)) { |
||||
value = value.toString(); |
||||
} |
||||
|
||||
if (isNumber(text)) { |
||||
text = text.toString(); |
||||
} |
||||
|
||||
if (regex) { |
||||
const matches = getAllMatches(value, regex); |
||||
if (!matches.length) { |
||||
continue; |
||||
} |
||||
|
||||
const valueGroup = matches.find((m) => m.groups && m.groups.value); |
||||
const textGroup = matches.find((m) => m.groups && m.groups.text); |
||||
const firstMatch = matches.find((m) => m.length > 1); |
||||
const manyMatches = matches.length > 1 && firstMatch; |
||||
|
||||
if (valueGroup || textGroup) { |
||||
value = valueGroup?.groups?.value ?? textGroup?.groups?.text; |
||||
text = textGroup?.groups?.text ?? valueGroup?.groups?.value; |
||||
} else if (manyMatches) { |
||||
for (let j = 0; j < matches.length; j++) { |
||||
const match = matches[j]; |
||||
options.push({ label: match[1], value: match[1] }); |
||||
} |
||||
continue; |
||||
} else if (firstMatch) { |
||||
text = firstMatch[1]; |
||||
value = firstMatch[1]; |
||||
} |
||||
} |
||||
|
||||
options.push({ label: text, value: value }); |
||||
} |
||||
|
||||
options = uniqBy(options, 'value'); |
||||
return sortVariableValues(options, sort); |
||||
}; |
||||
|
||||
const getAllMatches = (str: string, regex: RegExp): RegExpExecArray[] => { |
||||
const results: RegExpExecArray[] = []; |
||||
let matches = null; |
||||
|
||||
regex.lastIndex = 0; |
||||
|
||||
do { |
||||
matches = regex.exec(str); |
||||
if (matches) { |
||||
results.push(matches); |
||||
} |
||||
} while (regex.global && matches && matches[0] !== '' && matches[0] !== undefined); |
||||
|
||||
return results; |
||||
}; |
||||
|
||||
export const sortVariableValues = (options: any[], sortOrder: VariableSort) => { |
||||
if (sortOrder === VariableSort.disabled) { |
||||
return options; |
||||
} |
||||
|
||||
const sortType = Math.ceil(sortOrder / 2); |
||||
const reverseSort = sortOrder % 2 === 0; |
||||
|
||||
if (sortType === 1) { |
||||
options = sortBy(options, 'text'); |
||||
} else if (sortType === 2) { |
||||
options = sortBy(options, (opt) => { |
||||
if (!opt.text) { |
||||
return -1; |
||||
} |
||||
|
||||
const matches = opt.text.match(/.*?(\d+).*/); |
||||
if (!matches || matches.length < 2) { |
||||
return -1; |
||||
} else { |
||||
return parseInt(matches[1], 10); |
||||
} |
||||
}); |
||||
} else if (sortType === 3) { |
||||
options = sortBy(options, (opt) => { |
||||
return toLower(opt.text); |
||||
}); |
||||
} |
||||
|
||||
if (reverseSort) { |
||||
options = options.reverse(); |
||||
} |
||||
|
||||
return options; |
||||
}; |
Loading…
Reference in new issue