mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Variables: Adds new Api that allows proper QueryEditors for Query variables (#28217)
* Initial * WIP * wip * Refactor: fixing types * Refactor: Fixed more typings * Feature: Moves TestData to new API * Feature: Moves CloudMonitoringDatasource to new API * Feature: Moves PrometheusDatasource to new Variables API * Refactor: Clean up comments * Refactor: changes to QueryEditorProps instead * Refactor: cleans up testdata, prometheus and cloud monitoring variable support * Refactor: adds variableQueryRunner * Refactor: adds props to VariableQueryEditor * Refactor: reverted Loki editor * Refactor: refactor queryrunner into smaller pieces * Refactor: adds upgrade query thunk * Tests: Updates old tests * Docs: fixes build errors for exported api * Tests: adds guard tests * Tests: adds QueryRunner tests * Tests: fixes broken tests * Tests: adds variableQueryObserver tests * Test: adds tests for operator functions * Test: adds VariableQueryRunner tests * Refactor: renames dataSource * Refactor: adds definition for standard variable support * Refactor: adds cancellation to OptionPicker * Refactor: changes according to Dominiks suggestion * Refactor:tt * Refactor: adds tests for factories * Refactor: restructuring a bit * Refactor: renames variableQueryRunner.ts * Refactor: adds quick exit when runRequest returns errors * Refactor: using TextArea from grafana/ui * Refactor: changed from interfaces to classes instead * Tests: fixes broken test * Docs: fixes doc issue count * Docs: fixes doc issue count * Refactor: Adds check for self referencing queries * Tests: fixed unused variable * Refactor: Changes comments
This commit is contained in:
@@ -4,12 +4,13 @@ import { GrafanaPlugin, PluginMeta } from './plugin';
|
||||
import { PanelData } from './panel';
|
||||
import { LogRowModel } from './logs';
|
||||
import { AnnotationEvent, AnnotationSupport } from './annotations';
|
||||
import { KeyValue, LoadingState, TableData, TimeSeries, DataTopic } from './data';
|
||||
import { DataTopic, KeyValue, LoadingState, TableData, TimeSeries } from './data';
|
||||
import { DataFrame, DataFrameDTO } from './dataFrame';
|
||||
import { RawTimeRange, TimeRange } from './time';
|
||||
import { ScopedVars } from './ScopedVars';
|
||||
import { CoreApp } from './app';
|
||||
import { LiveChannelSupport } from './live';
|
||||
import { CustomVariableSupport, DataSourceVariableSupport, StandardVariableSupport } from './variables';
|
||||
|
||||
export interface DataSourcePluginOptionsEditorProps<JSONData = DataSourceJsonData, SecureJSONData = {}> {
|
||||
options: DataSourceSettings<JSONData, SecureJSONData>;
|
||||
@@ -79,6 +80,9 @@ export class DataSourcePlugin<
|
||||
return this;
|
||||
}
|
||||
|
||||
/*
|
||||
* @deprecated -- prefer using {@link StandardVariableSupport} or {@link CustomVariableSupport} or {@link DataSourceVariableSupport} in data source instead
|
||||
* */
|
||||
setVariableQueryEditor(VariableQueryEditor: any) {
|
||||
this.components.VariableQueryEditor = VariableQueryEditor;
|
||||
return this;
|
||||
@@ -295,6 +299,15 @@ export abstract class DataSourceApi<
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
channelSupport?: LiveChannelSupport;
|
||||
|
||||
/**
|
||||
* Defines new variable support
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
variables?:
|
||||
| StandardVariableSupport<DataSourceApi<TQuery, TOptions>>
|
||||
| CustomVariableSupport<DataSourceApi<TQuery, TOptions>>
|
||||
| DataSourceVariableSupport<DataSourceApi<TQuery, TOptions>>;
|
||||
}
|
||||
|
||||
export interface MetadataInspectorProps<
|
||||
@@ -311,12 +324,13 @@ export interface MetadataInspectorProps<
|
||||
export interface QueryEditorProps<
|
||||
DSType extends DataSourceApi<TQuery, TOptions>,
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData,
|
||||
TVQuery extends DataQuery = TQuery
|
||||
> {
|
||||
datasource: DSType;
|
||||
query: TQuery;
|
||||
query: TVQuery;
|
||||
onRunQuery: () => void;
|
||||
onChange: (value: TQuery) => void;
|
||||
onChange: (value: TVQuery) => void;
|
||||
onBlur?: () => void;
|
||||
/**
|
||||
* Contains query response filtered by refId of QueryResultBase and possible query error
|
||||
|
||||
@@ -28,5 +28,6 @@ export * from './trace';
|
||||
export * from './explore';
|
||||
export * from './legacyEvents';
|
||||
export * from './live';
|
||||
export * from './variables';
|
||||
|
||||
export { GrafanaConfig, BuildInfo, FeatureToggles, LicenseInfo } from './config';
|
||||
|
||||
99
packages/grafana-data/src/types/variables.ts
Normal file
99
packages/grafana-data/src/types/variables.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { ComponentType } from 'react';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import {
|
||||
DataQuery,
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
DataSourceApi,
|
||||
DataSourceJsonData,
|
||||
DataSourceOptionsType,
|
||||
DataSourceQueryType,
|
||||
QueryEditorProps,
|
||||
} from './datasource';
|
||||
|
||||
/**
|
||||
* Enum with the different variable support types
|
||||
*
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
export enum VariableSupportType {
|
||||
Legacy = 'legacy',
|
||||
Standard = 'standard',
|
||||
Custom = 'custom',
|
||||
Datasource = 'datasource',
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for VariableSupport classes
|
||||
*
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
export abstract class VariableSupportBase<
|
||||
DSType extends DataSourceApi<TQuery, TOptions>,
|
||||
TQuery extends DataQuery = DataSourceQueryType<DSType>,
|
||||
TOptions extends DataSourceJsonData = DataSourceOptionsType<DSType>
|
||||
> {
|
||||
abstract getType(): VariableSupportType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend this class in a data source plugin to use the standard query editor for Query variables
|
||||
*
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
export abstract class StandardVariableSupport<
|
||||
DSType extends DataSourceApi<TQuery, TOptions>,
|
||||
TQuery extends DataQuery = DataSourceQueryType<DSType>,
|
||||
TOptions extends DataSourceJsonData = DataSourceOptionsType<DSType>
|
||||
> extends VariableSupportBase<DSType, TQuery, TOptions> {
|
||||
getType(): VariableSupportType {
|
||||
return VariableSupportType.Standard;
|
||||
}
|
||||
|
||||
abstract toDataQuery(query: StandardVariableQuery): TQuery;
|
||||
query?(request: DataQueryRequest<TQuery>): Observable<DataQueryResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend this class in a data source plugin to use a customized query editor for Query variables
|
||||
*
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
export abstract class CustomVariableSupport<
|
||||
DSType extends DataSourceApi<TQuery, TOptions>,
|
||||
VariableQuery extends DataQuery = any,
|
||||
TQuery extends DataQuery = DataSourceQueryType<DSType>,
|
||||
TOptions extends DataSourceJsonData = DataSourceOptionsType<DSType>
|
||||
> extends VariableSupportBase<DSType, TQuery, TOptions> {
|
||||
getType(): VariableSupportType {
|
||||
return VariableSupportType.Custom;
|
||||
}
|
||||
|
||||
abstract editor: ComponentType<QueryEditorProps<DSType, TQuery, TOptions, VariableQuery>>;
|
||||
abstract query(request: DataQueryRequest<VariableQuery>): Observable<DataQueryResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend this class in a data source plugin to use the query editor in the data source plugin for Query variables
|
||||
*
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
export abstract class DataSourceVariableSupport<
|
||||
DSType extends DataSourceApi<TQuery, TOptions>,
|
||||
TQuery extends DataQuery = DataSourceQueryType<DSType>,
|
||||
TOptions extends DataSourceJsonData = DataSourceOptionsType<DSType>
|
||||
> extends VariableSupportBase<DSType, TQuery, TOptions> {
|
||||
getType(): VariableSupportType {
|
||||
return VariableSupportType.Datasource;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the standard DatQuery used by data source plugins that implement StandardVariableSupport
|
||||
*
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
export interface StandardVariableQuery extends DataQuery {
|
||||
query: string;
|
||||
}
|
||||
@@ -28,10 +28,10 @@ import _ from 'lodash';
|
||||
import {
|
||||
AppEvents,
|
||||
setLocale,
|
||||
setTimeZoneResolver,
|
||||
standardEditorsRegistry,
|
||||
standardFieldConfigEditorRegistry,
|
||||
standardTransformersRegistry,
|
||||
setTimeZoneResolver,
|
||||
} from '@grafana/data';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { checkBrowserCompatibility } from 'app/core/utils/browser';
|
||||
@@ -45,12 +45,13 @@ import { reportPerformance } from './core/services/echo/EchoSrv';
|
||||
import { PerformanceBackend } from './core/services/echo/backends/PerformanceBackend';
|
||||
import 'app/routes/GrafanaCtrl';
|
||||
import 'app/features/all';
|
||||
import { getStandardFieldConfigs, getStandardOptionEditors, getScrollbarWidth } from '@grafana/ui';
|
||||
import { getScrollbarWidth, getStandardFieldConfigs, getStandardOptionEditors } from '@grafana/ui';
|
||||
import { getDefaultVariableAdapters, variableAdapters } from './features/variables/adapters';
|
||||
import { initDevFeatures } from './dev';
|
||||
import { getStandardTransformers } from 'app/core/utils/standardTransformers';
|
||||
import { SentryEchoBackend } from './core/services/echo/backends/sentry/SentryBackend';
|
||||
import { monkeyPatchInjectorWithPreAssignedBindings } from './core/injectorMonkeyPatch';
|
||||
import { setVariableQueryRunner, VariableQueryRunner } from './features/variables/query/VariableQueryRunner';
|
||||
|
||||
// add move to lodash for backward compatabiltiy
|
||||
// @ts-ignore
|
||||
@@ -101,6 +102,7 @@ export class GrafanaApp {
|
||||
standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs);
|
||||
standardTransformersRegistry.setInit(getStandardTransformers);
|
||||
variableAdapters.setInit(getDefaultVariableAdapters);
|
||||
setVariableQueryRunner(new VariableQueryRunner());
|
||||
|
||||
app.config(
|
||||
(
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
// Libraries
|
||||
import { Observable, of, timer, merge, from } from 'rxjs';
|
||||
import { map as isArray, isString } from 'lodash';
|
||||
import { map, catchError, takeUntil, mapTo, share, finalize, tap } from 'rxjs/operators';
|
||||
import { from, merge, Observable, of, timer } from 'rxjs';
|
||||
import { isString, map as isArray } from 'lodash';
|
||||
import { catchError, finalize, map, mapTo, share, takeUntil, tap } from 'rxjs/operators';
|
||||
// Utils & Services
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
// Types
|
||||
import {
|
||||
DataSourceApi,
|
||||
DataFrame,
|
||||
DataQueryError,
|
||||
DataQueryRequest,
|
||||
PanelData,
|
||||
DataQueryResponse,
|
||||
DataQueryResponseData,
|
||||
DataQueryError,
|
||||
LoadingState,
|
||||
dateMath,
|
||||
toDataFrame,
|
||||
DataFrame,
|
||||
DataSourceApi,
|
||||
DataTopic,
|
||||
dateMath,
|
||||
guessFieldTypes,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { toDataQueryError } from '@grafana/runtime';
|
||||
import { emitDataRequestEvent } from './analyticsProcessor';
|
||||
import { ExpressionDatasourceID, expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
|
||||
import { expressionDatasource, ExpressionDatasourceID } from 'app/features/expressions/ExpressionDatasource';
|
||||
import { ExpressionQuery } from 'app/features/expressions/types';
|
||||
|
||||
type MapOfResponsePackets = { [str: string]: DataQueryResponse };
|
||||
@@ -97,7 +97,11 @@ export function processResponsePacket(packet: DataQueryResponse, state: RunningQ
|
||||
* Will emit a loading state if no response after 50ms
|
||||
* Cancel any still running network requests on unsubscribe (using request.requestId)
|
||||
*/
|
||||
export function runRequest(datasource: DataSourceApi, request: DataQueryRequest): Observable<PanelData> {
|
||||
export function runRequest(
|
||||
datasource: DataSourceApi,
|
||||
request: DataQueryRequest,
|
||||
queryFunction?: typeof datasource.query
|
||||
): Observable<PanelData> {
|
||||
let state: RunningQueryState = {
|
||||
panelData: {
|
||||
state: LoadingState.Loading,
|
||||
@@ -115,7 +119,7 @@ export function runRequest(datasource: DataSourceApi, request: DataQueryRequest)
|
||||
return of(state.panelData);
|
||||
}
|
||||
|
||||
const dataObservable = callQueryMethod(datasource, request).pipe(
|
||||
const dataObservable = callQueryMethod(datasource, request, queryFunction).pipe(
|
||||
// Transform response packets into PanelData with merged results
|
||||
map((packet: DataQueryResponse) => {
|
||||
if (!isArray(packet.data)) {
|
||||
@@ -157,7 +161,11 @@ function cancelNetworkRequestsOnUnsubscribe(req: DataQueryRequest) {
|
||||
};
|
||||
}
|
||||
|
||||
export function callQueryMethod(datasource: DataSourceApi, request: DataQueryRequest) {
|
||||
export function callQueryMethod(
|
||||
datasource: DataSourceApi,
|
||||
request: DataQueryRequest,
|
||||
queryFunction?: typeof datasource.query
|
||||
) {
|
||||
// If any query has an expression, use the expression endpoint
|
||||
for (const target of request.targets) {
|
||||
if (target.datasource === ExpressionDatasourceID) {
|
||||
@@ -166,7 +174,7 @@ export function callQueryMethod(datasource: DataSourceApi, request: DataQueryReq
|
||||
}
|
||||
|
||||
// Otherwise it is a standard datasource request
|
||||
const returnVal = datasource.query(request);
|
||||
const returnVal = queryFunction ? queryFunction(request) : datasource.query(request);
|
||||
return from(returnVal);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import coreModule from 'app/core/core_module';
|
||||
import { importDataSourcePlugin } from './plugin_loader';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import DefaultVariableQueryEditor from '../variables/editor/DefaultVariableQueryEditor';
|
||||
import { LegacyVariableQueryEditor } from '../variables/editor/LegacyVariableQueryEditor';
|
||||
import { DataSourcePluginMeta } from '@grafana/data';
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
|
||||
@@ -11,7 +11,7 @@ async function loadComponent(meta: DataSourcePluginMeta) {
|
||||
if (dsPlugin.components.VariableQueryEditor) {
|
||||
return dsPlugin.components.VariableQueryEditor;
|
||||
} else {
|
||||
return DefaultVariableQueryEditor;
|
||||
return LegacyVariableQueryEditor;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { VariableQueryProps } from 'app/types/plugins';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
export default class DefaultVariableQueryEditor extends PureComponent<VariableQueryProps, any> {
|
||||
constructor(props: VariableQueryProps) {
|
||||
super(props);
|
||||
this.state = { value: props.query };
|
||||
}
|
||||
|
||||
onChange = (event: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
this.setState({ value: event.currentTarget.value });
|
||||
};
|
||||
|
||||
onBlur = (event: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
this.props.onChange(event.currentTarget.value, event.currentTarget.value);
|
||||
};
|
||||
|
||||
getLineCount() {
|
||||
const { value } = this.state;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value.split('\n').length;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="gf-form">
|
||||
<span className="gf-form-label width-10">Query</span>
|
||||
<textarea
|
||||
rows={this.getLineCount()}
|
||||
className="gf-form-input"
|
||||
value={this.state.value}
|
||||
onChange={this.onChange}
|
||||
onBlur={this.onBlur}
|
||||
placeholder="metric name or tags query"
|
||||
required
|
||||
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { VariableQueryProps } from 'app/types/plugins';
|
||||
import { InlineField, TextArea, useStyles } from '@grafana/ui';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { css } from 'emotion';
|
||||
|
||||
export const LEGACY_VARIABLE_QUERY_EDITOR_NAME = 'Grafana-LegacyVariableQueryEditor';
|
||||
|
||||
export const LegacyVariableQueryEditor: FC<VariableQueryProps> = ({ onChange, query }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
const [value, setValue] = useState(query);
|
||||
const onValueChange = useCallback(
|
||||
(event: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
setValue(event.currentTarget.value);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
const onBlur = useCallback(
|
||||
(event: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
onChange(event.currentTarget.value, event.currentTarget.value);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="gf-form">
|
||||
<InlineField label="Query" labelWidth={20} grow={false} className={styles.inlineFieldOverride}>
|
||||
<span hidden />
|
||||
</InlineField>
|
||||
<TextArea
|
||||
rows={getLineCount(value)}
|
||||
className="gf-form-input"
|
||||
value={value}
|
||||
onChange={onValueChange}
|
||||
onBlur={onBlur}
|
||||
placeholder="metric name or tags query"
|
||||
required
|
||||
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getStyles(theme: GrafanaTheme) {
|
||||
return {
|
||||
inlineFieldOverride: css`
|
||||
margin: 0;
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
LegacyVariableQueryEditor.displayName = LEGACY_VARIABLE_QUERY_EDITOR_NAME;
|
||||
|
||||
const getLineCount = (value: any) => {
|
||||
if (value && typeof value === 'string') {
|
||||
return value.split('\n').length;
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
@@ -92,6 +92,11 @@ export class VariableEditorList extends PureComponent<Props> {
|
||||
<tbody>
|
||||
{this.props.variables.map((state, index) => {
|
||||
const variable = state as QueryVariableModel;
|
||||
const definition = variable.definition
|
||||
? variable.definition
|
||||
: typeof variable.query === 'string'
|
||||
? variable.query
|
||||
: '';
|
||||
const usages = getVariableUsages(variable.id, this.props.variables, this.props.dashboard);
|
||||
const passed = usages > 0 || isAdHoc(variable);
|
||||
return (
|
||||
@@ -115,7 +120,7 @@ export class VariableEditorList extends PureComponent<Props> {
|
||||
variable.name
|
||||
)}
|
||||
>
|
||||
{variable.definition ? variable.definition : variable.query}
|
||||
{definition}
|
||||
</td>
|
||||
|
||||
<td style={{ width: '1%' }}>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { VariableSupportType } from '@grafana/data';
|
||||
import { getVariableQueryEditor, StandardVariableQueryEditor } from './getVariableQueryEditor';
|
||||
import { LegacyVariableQueryEditor } from './LegacyVariableQueryEditor';
|
||||
|
||||
describe('getVariableQueryEditor', () => {
|
||||
describe('happy cases', () => {
|
||||
describe('when called with a data source with custom variable support', () => {
|
||||
it('then it should return correct editor', async () => {
|
||||
const editor: any = StandardVariableQueryEditor;
|
||||
const datasource: any = {
|
||||
variables: { getType: () => VariableSupportType.Custom, query: () => undefined, editor },
|
||||
};
|
||||
|
||||
const result = await getVariableQueryEditor(datasource);
|
||||
|
||||
expect(result).toBe(editor);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a data source with standard variable support', () => {
|
||||
it('then it should return correct editor', async () => {
|
||||
const editor: any = StandardVariableQueryEditor;
|
||||
const datasource: any = {
|
||||
variables: { getType: () => VariableSupportType.Standard, toDataQuery: () => undefined },
|
||||
};
|
||||
|
||||
const result = await getVariableQueryEditor(datasource);
|
||||
|
||||
expect(result).toBe(editor);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a data source with datasource variable support', () => {
|
||||
it('then it should return correct editor', async () => {
|
||||
const editor: any = StandardVariableQueryEditor;
|
||||
const plugin = { components: { QueryEditor: editor } };
|
||||
const importDataSourcePluginFunc = jest.fn().mockResolvedValue(plugin);
|
||||
const datasource: any = { variables: { getType: () => VariableSupportType.Datasource }, meta: {} };
|
||||
|
||||
const result = await getVariableQueryEditor(datasource, importDataSourcePluginFunc);
|
||||
|
||||
expect(result).toBe(editor);
|
||||
expect(importDataSourcePluginFunc).toHaveBeenCalledTimes(1);
|
||||
expect(importDataSourcePluginFunc).toHaveBeenCalledWith({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a data source with legacy variable support', () => {
|
||||
it('then it should return correct editor', async () => {
|
||||
const editor: any = StandardVariableQueryEditor;
|
||||
const plugin = { components: { VariableQueryEditor: editor } };
|
||||
const importDataSourcePluginFunc = jest.fn().mockResolvedValue(plugin);
|
||||
const datasource: any = { metricFindQuery: () => undefined, meta: {} };
|
||||
|
||||
const result = await getVariableQueryEditor(datasource, importDataSourcePluginFunc);
|
||||
|
||||
expect(result).toBe(editor);
|
||||
expect(importDataSourcePluginFunc).toHaveBeenCalledTimes(1);
|
||||
expect(importDataSourcePluginFunc).toHaveBeenCalledWith({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('negative cases', () => {
|
||||
describe('when variable support is not recognized', () => {
|
||||
it('then it should return null', async () => {
|
||||
const datasource: any = {};
|
||||
|
||||
const result = await getVariableQueryEditor(datasource);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a data source with datasource variable support but missing QueryEditor', () => {
|
||||
it('then it should return throw', async () => {
|
||||
const plugin = { components: {} };
|
||||
const importDataSourcePluginFunc = jest.fn().mockResolvedValue(plugin);
|
||||
const datasource: any = { variables: { getType: () => VariableSupportType.Datasource }, meta: {} };
|
||||
|
||||
await expect(getVariableQueryEditor(datasource, importDataSourcePluginFunc)).rejects.toThrow(
|
||||
new Error('Missing QueryEditor in plugin definition.')
|
||||
);
|
||||
expect(importDataSourcePluginFunc).toHaveBeenCalledTimes(1);
|
||||
expect(importDataSourcePluginFunc).toHaveBeenCalledWith({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a data source with legacy variable support but missing VariableQueryEditor', () => {
|
||||
it('then it should return LegacyVariableQueryEditor', async () => {
|
||||
const plugin = { components: {} };
|
||||
const importDataSourcePluginFunc = jest.fn().mockResolvedValue(plugin);
|
||||
const datasource: any = { metricFindQuery: () => undefined, meta: {} };
|
||||
|
||||
const result = await getVariableQueryEditor(datasource, importDataSourcePluginFunc);
|
||||
|
||||
expect(result).toBe(LegacyVariableQueryEditor);
|
||||
expect(importDataSourcePluginFunc).toHaveBeenCalledTimes(1);
|
||||
expect(importDataSourcePluginFunc).toHaveBeenCalledWith({});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { DataQuery, DataSourceApi, DataSourceJsonData, QueryEditorProps, StandardVariableQuery } from '@grafana/data';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
|
||||
import { LegacyVariableQueryEditor } from './LegacyVariableQueryEditor';
|
||||
import {
|
||||
hasCustomVariableSupport,
|
||||
hasDatasourceVariableSupport,
|
||||
hasLegacyVariableSupport,
|
||||
hasStandardVariableSupport,
|
||||
} from '../guard';
|
||||
import { importDataSourcePlugin } from '../../plugins/plugin_loader';
|
||||
import { VariableQueryEditorType } from '../types';
|
||||
|
||||
export async function getVariableQueryEditor<
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData,
|
||||
VariableQuery extends DataQuery = TQuery
|
||||
>(
|
||||
datasource: DataSourceApi<TQuery, TOptions>,
|
||||
importDataSourcePluginFunc = importDataSourcePlugin
|
||||
): Promise<VariableQueryEditorType> {
|
||||
if (hasCustomVariableSupport(datasource)) {
|
||||
return datasource.variables.editor;
|
||||
}
|
||||
|
||||
if (hasDatasourceVariableSupport(datasource)) {
|
||||
const dsPlugin = await importDataSourcePluginFunc(datasource.meta!);
|
||||
|
||||
if (!dsPlugin.components.QueryEditor) {
|
||||
throw new Error('Missing QueryEditor in plugin definition.');
|
||||
}
|
||||
|
||||
return dsPlugin.components.QueryEditor ?? null;
|
||||
}
|
||||
|
||||
if (hasStandardVariableSupport(datasource)) {
|
||||
return StandardVariableQueryEditor;
|
||||
}
|
||||
|
||||
if (hasLegacyVariableSupport(datasource)) {
|
||||
const dsPlugin = await importDataSourcePluginFunc(datasource.meta!);
|
||||
return dsPlugin.components.VariableQueryEditor ?? LegacyVariableQueryEditor;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function StandardVariableQueryEditor<
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
>({
|
||||
datasource: propsDatasource,
|
||||
query: propsQuery,
|
||||
onChange: propsOnChange,
|
||||
}: QueryEditorProps<any, TQuery, TOptions, StandardVariableQuery>) {
|
||||
const onChange = useCallback(
|
||||
(query: any) => {
|
||||
propsOnChange({ refId: 'StandardVariableQuery', query });
|
||||
},
|
||||
[propsOnChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<LegacyVariableQueryEditor
|
||||
query={propsQuery.query}
|
||||
onChange={onChange}
|
||||
datasource={propsDatasource}
|
||||
templateSrv={getTemplateSrv()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
243
public/app/features/variables/guard.test.ts
Normal file
243
public/app/features/variables/guard.test.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import {
|
||||
hasCustomVariableSupport,
|
||||
hasDatasourceVariableSupport,
|
||||
hasLegacyVariableSupport,
|
||||
hasStandardVariableSupport,
|
||||
isLegacyQueryEditor,
|
||||
isQueryEditor,
|
||||
} from './guard';
|
||||
import { LegacyVariableQueryEditor } from './editor/LegacyVariableQueryEditor';
|
||||
import { StandardVariableQueryEditor } from './editor/getVariableQueryEditor';
|
||||
import { VariableSupportType } from '@grafana/data';
|
||||
|
||||
describe('type guards', () => {
|
||||
describe('hasLegacyVariableSupport', () => {
|
||||
describe('when called with a legacy data source', () => {
|
||||
it('should return true', () => {
|
||||
const datasource: any = { metricFindQuery: () => undefined };
|
||||
expect(hasLegacyVariableSupport(datasource)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with data source without metricFindQuery function', () => {
|
||||
it('should return false', () => {
|
||||
const datasource: any = {};
|
||||
expect(hasLegacyVariableSupport(datasource)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a legacy data source with variable support', () => {
|
||||
it('should return false', () => {
|
||||
const datasource: any = { metricFindQuery: () => undefined, variables: {} };
|
||||
expect(hasLegacyVariableSupport(datasource)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasStandardVariableSupport', () => {
|
||||
describe('when called with a data source with standard variable support', () => {
|
||||
it('should return true', () => {
|
||||
const datasource: any = {
|
||||
metricFindQuery: () => undefined,
|
||||
variables: { getType: () => VariableSupportType.Standard, toDataQuery: () => undefined },
|
||||
};
|
||||
expect(hasStandardVariableSupport(datasource)).toBe(true);
|
||||
});
|
||||
|
||||
describe('and with a custom query', () => {
|
||||
it('should return true', () => {
|
||||
const datasource: any = {
|
||||
metricFindQuery: () => undefined,
|
||||
variables: {
|
||||
getType: () => VariableSupportType.Standard,
|
||||
toDataQuery: () => undefined,
|
||||
query: () => undefined,
|
||||
},
|
||||
};
|
||||
expect(hasStandardVariableSupport(datasource)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a data source with partial standard variable support', () => {
|
||||
it('should return false', () => {
|
||||
const datasource: any = {
|
||||
metricFindQuery: () => undefined,
|
||||
variables: { getType: () => VariableSupportType.Standard, query: () => undefined },
|
||||
};
|
||||
expect(hasStandardVariableSupport(datasource)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a data source without standard variable support', () => {
|
||||
it('should return false', () => {
|
||||
const datasource: any = { metricFindQuery: () => undefined };
|
||||
expect(hasStandardVariableSupport(datasource)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasCustomVariableSupport', () => {
|
||||
describe('when called with a data source with custom variable support', () => {
|
||||
it('should return true', () => {
|
||||
const datasource: any = {
|
||||
metricFindQuery: () => undefined,
|
||||
variables: { getType: () => VariableSupportType.Custom, query: () => undefined, editor: {} },
|
||||
};
|
||||
expect(hasCustomVariableSupport(datasource)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a data source with custom variable support but without editor', () => {
|
||||
it('should return false', () => {
|
||||
const datasource: any = {
|
||||
metricFindQuery: () => undefined,
|
||||
variables: { getType: () => VariableSupportType.Custom, query: () => undefined },
|
||||
};
|
||||
expect(hasCustomVariableSupport(datasource)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a data source with custom variable support but without query', () => {
|
||||
it('should return false', () => {
|
||||
const datasource: any = {
|
||||
metricFindQuery: () => undefined,
|
||||
variables: { getType: () => VariableSupportType.Custom, editor: {} },
|
||||
};
|
||||
expect(hasCustomVariableSupport(datasource)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a data source without custom variable support', () => {
|
||||
it('should return false', () => {
|
||||
const datasource: any = { metricFindQuery: () => undefined };
|
||||
expect(hasCustomVariableSupport(datasource)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasDatasourceVariableSupport', () => {
|
||||
describe('when called with a data source with datasource variable support', () => {
|
||||
it('should return true', () => {
|
||||
const datasource: any = {
|
||||
metricFindQuery: () => undefined,
|
||||
variables: { getType: () => VariableSupportType.Datasource },
|
||||
};
|
||||
expect(hasDatasourceVariableSupport(datasource)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a data source without datasource variable support', () => {
|
||||
it('should return false', () => {
|
||||
const datasource: any = { metricFindQuery: () => undefined };
|
||||
expect(hasDatasourceVariableSupport(datasource)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLegacyQueryEditor', () => {
|
||||
describe('happy cases', () => {
|
||||
describe('when called with a legacy query editor but without a legacy data source', () => {
|
||||
it('then is should return true', () => {
|
||||
const component: any = LegacyVariableQueryEditor;
|
||||
const datasource: any = {};
|
||||
|
||||
expect(isLegacyQueryEditor(component, datasource)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a legacy data source but without a legacy query editor', () => {
|
||||
it('then is should return true', () => {
|
||||
const component: any = StandardVariableQueryEditor;
|
||||
const datasource: any = { metricFindQuery: () => undefined };
|
||||
|
||||
expect(isLegacyQueryEditor(component, datasource)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('negative cases', () => {
|
||||
describe('when called without component', () => {
|
||||
it('then is should return false', () => {
|
||||
const component: any = null;
|
||||
const datasource: any = { metricFindQuery: () => undefined };
|
||||
|
||||
expect(isLegacyQueryEditor(component, datasource)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called without a legacy query editor and without a legacy data source', () => {
|
||||
it('then is should return false', () => {
|
||||
const component: any = StandardVariableQueryEditor;
|
||||
const datasource: any = {};
|
||||
|
||||
expect(isLegacyQueryEditor(component, datasource)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isQueryEditor', () => {
|
||||
describe('happy cases', () => {
|
||||
describe('when called without a legacy editor and with a data source with standard variable support', () => {
|
||||
it('then is should return true', () => {
|
||||
const component: any = StandardVariableQueryEditor;
|
||||
const datasource: any = {
|
||||
variables: { getType: () => VariableSupportType.Standard, toDataQuery: () => undefined },
|
||||
};
|
||||
|
||||
expect(isQueryEditor(component, datasource)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called without a legacy editor and with a data source with custom variable support', () => {
|
||||
it('then is should return true', () => {
|
||||
const component: any = StandardVariableQueryEditor;
|
||||
const datasource: any = {
|
||||
variables: { getType: () => VariableSupportType.Custom, query: () => undefined, editor: {} },
|
||||
};
|
||||
|
||||
expect(isQueryEditor(component, datasource)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called without a legacy editor and with a data source with datasource variable support', () => {
|
||||
it('then is should return true', () => {
|
||||
const component: any = StandardVariableQueryEditor;
|
||||
const datasource: any = { variables: { getType: () => VariableSupportType.Datasource } };
|
||||
|
||||
expect(isQueryEditor(component, datasource)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('negative cases', () => {
|
||||
describe('when called without component', () => {
|
||||
it('then is should return false', () => {
|
||||
const component: any = null;
|
||||
const datasource: any = { metricFindQuery: () => undefined };
|
||||
|
||||
expect(isQueryEditor(component, datasource)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a legacy query editor', () => {
|
||||
it('then is should return false', () => {
|
||||
const component: any = LegacyVariableQueryEditor;
|
||||
const datasource: any = { variables: { getType: () => VariableSupportType.Datasource } };
|
||||
|
||||
expect(isQueryEditor(component, datasource)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called without a legacy query editor but with a legacy data source', () => {
|
||||
it('then is should return false', () => {
|
||||
const component: any = StandardVariableQueryEditor;
|
||||
const datasource: any = { metricFindQuery: () => undefined };
|
||||
|
||||
expect(isQueryEditor(component, datasource)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,29 @@
|
||||
import { ComponentType } from 'react';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
CustomVariableSupport,
|
||||
DataQuery,
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
DataSourceApi,
|
||||
DataSourceJsonData,
|
||||
MetricFindValue,
|
||||
QueryEditorProps,
|
||||
StandardVariableQuery,
|
||||
StandardVariableSupport,
|
||||
VariableSupportType,
|
||||
} from '@grafana/data';
|
||||
|
||||
import {
|
||||
AdHocVariableModel,
|
||||
ConstantVariableModel,
|
||||
QueryVariableModel,
|
||||
VariableModel,
|
||||
VariableQueryEditorType,
|
||||
VariableWithMultiSupport,
|
||||
} from './types';
|
||||
import { VariableQueryProps } from '../../types';
|
||||
import { LEGACY_VARIABLE_QUERY_EDITOR_NAME } from './editor/LegacyVariableQueryEditor';
|
||||
|
||||
export const isQuery = (model: VariableModel): model is QueryVariableModel => {
|
||||
return model.type === 'query';
|
||||
@@ -22,3 +41,140 @@ export const isMulti = (model: VariableModel): model is VariableWithMultiSupport
|
||||
const withMulti = model as VariableWithMultiSupport;
|
||||
return withMulti.hasOwnProperty('multi') && typeof withMulti.multi === 'boolean';
|
||||
};
|
||||
|
||||
interface DataSourceWithLegacyVariableSupport<
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
> extends DataSourceApi<TQuery, TOptions> {
|
||||
metricFindQuery(query: any, options?: any): Promise<MetricFindValue[]>;
|
||||
variables: undefined;
|
||||
}
|
||||
|
||||
interface DataSourceWithStandardVariableSupport<
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
> extends DataSourceApi<TQuery, TOptions> {
|
||||
variables: {
|
||||
getType(): VariableSupportType;
|
||||
toDataQuery(query: StandardVariableQuery): TQuery;
|
||||
query(request: DataQueryRequest<TQuery>): Observable<DataQueryResponse>;
|
||||
};
|
||||
}
|
||||
|
||||
interface DataSourceWithCustomVariableSupport<
|
||||
VariableQuery extends DataQuery = any,
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
> extends DataSourceApi<TQuery, TOptions> {
|
||||
variables: {
|
||||
getType(): VariableSupportType;
|
||||
editor: ComponentType<QueryEditorProps<any, TQuery, TOptions, VariableQuery>>;
|
||||
query(request: DataQueryRequest<TQuery>): Observable<DataQueryResponse>;
|
||||
};
|
||||
}
|
||||
|
||||
interface DataSourceWithDatasourceVariableSupport<
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
> extends DataSourceApi<TQuery, TOptions> {
|
||||
variables: {
|
||||
getType(): VariableSupportType;
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* The following guard function are both TypeScript type guards.
|
||||
* They also make the basis for the logic used by variableQueryRunner and determining which QueryEditor to use
|
||||
* */
|
||||
export const hasLegacyVariableSupport = <
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
>(
|
||||
datasource: DataSourceApi<TQuery, TOptions>
|
||||
): datasource is DataSourceWithLegacyVariableSupport<TQuery, TOptions> => {
|
||||
return Boolean(datasource.metricFindQuery) && !Boolean(datasource.variables);
|
||||
};
|
||||
|
||||
export const hasStandardVariableSupport = <
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
>(
|
||||
datasource: DataSourceApi<TQuery, TOptions>
|
||||
): datasource is DataSourceWithStandardVariableSupport<TQuery, TOptions> => {
|
||||
if (!datasource.variables) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (datasource.variables.getType() !== VariableSupportType.Standard) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const variableSupport = datasource.variables as StandardVariableSupport<DataSourceApi<TQuery, TOptions>>;
|
||||
|
||||
return Boolean(variableSupport.toDataQuery);
|
||||
};
|
||||
|
||||
export const hasCustomVariableSupport = <
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
>(
|
||||
datasource: DataSourceApi<TQuery, TOptions>
|
||||
): datasource is DataSourceWithCustomVariableSupport<any, TQuery, TOptions> => {
|
||||
if (!datasource.variables) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (datasource.variables.getType() !== VariableSupportType.Custom) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const variableSupport = datasource.variables as CustomVariableSupport<DataSourceApi<TQuery, TOptions>>;
|
||||
|
||||
return Boolean(variableSupport.query) && Boolean(variableSupport.editor);
|
||||
};
|
||||
|
||||
export const hasDatasourceVariableSupport = <
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
>(
|
||||
datasource: DataSourceApi<TQuery, TOptions>
|
||||
): datasource is DataSourceWithDatasourceVariableSupport<TQuery, TOptions> => {
|
||||
if (!datasource.variables) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return datasource.variables.getType() === VariableSupportType.Datasource;
|
||||
};
|
||||
|
||||
export function isLegacyQueryEditor<
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
>(
|
||||
component: VariableQueryEditorType,
|
||||
datasource: DataSourceApi<TQuery, TOptions>
|
||||
): component is ComponentType<VariableQueryProps> {
|
||||
if (!component) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return component.displayName === LEGACY_VARIABLE_QUERY_EDITOR_NAME || hasLegacyVariableSupport(datasource);
|
||||
}
|
||||
|
||||
export function isQueryEditor<
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
>(
|
||||
component: VariableQueryEditorType,
|
||||
datasource: DataSourceApi<TQuery, TOptions>
|
||||
): component is ComponentType<QueryEditorProps<any>> {
|
||||
if (!component) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
component.displayName !== LEGACY_VARIABLE_QUERY_EDITOR_NAME &&
|
||||
(hasDatasourceVariableSupport(datasource) ||
|
||||
hasStandardVariableSupport(datasource) ||
|
||||
hasCustomVariableSupport(datasource))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||
import { StoreState } from 'app/types';
|
||||
import { ClickOutsideWrapper } from '@grafana/ui';
|
||||
import { LoadingState } from '@grafana/data';
|
||||
|
||||
import { StoreState } from 'app/types';
|
||||
import { VariableLink } from '../shared/VariableLink';
|
||||
import { VariableInput } from '../shared/VariableInput';
|
||||
import { commitChangesToVariable, filterOrSearchOptions, navigateOptions, toggleAndFetchTag } from './actions';
|
||||
@@ -11,7 +13,8 @@ import { VariableOptions } from '../shared/VariableOptions';
|
||||
import { isQuery } from '../../guard';
|
||||
import { VariablePickerProps } from '../types';
|
||||
import { formatVariableLabel } from '../../shared/formatVariable';
|
||||
import { LoadingState } from '@grafana/data';
|
||||
import { toVariableIdentifier } from '../../state/types';
|
||||
import { getVariableQueryRunner } from '../../query/VariableQueryRunner';
|
||||
|
||||
interface OwnProps extends VariablePickerProps<VariableWithMultiSupport> {}
|
||||
|
||||
@@ -70,9 +73,21 @@ export class OptionsPickerUnconnected extends PureComponent<Props> {
|
||||
const tags = getSelectedTags(variable);
|
||||
const loading = variable.state === LoadingState.Loading;
|
||||
|
||||
return <VariableLink text={linkText} tags={tags} onClick={this.onShowOptions} loading={loading} />;
|
||||
return (
|
||||
<VariableLink
|
||||
text={linkText}
|
||||
tags={tags}
|
||||
onClick={this.onShowOptions}
|
||||
loading={loading}
|
||||
onCancel={this.onCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
onCancel = () => {
|
||||
getVariableQueryRunner().cancelRequest(toVariableIdentifier(this.props.variable));
|
||||
};
|
||||
|
||||
renderOptions(showOptions: boolean, picker: OptionsPickerState) {
|
||||
if (!showOptions) {
|
||||
return null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { FC, MouseEvent, useCallback } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { getTagColorsFromName, Icon, useStyles } from '@grafana/ui';
|
||||
import { getTagColorsFromName, Icon, Tooltip, useStyles } from '@grafana/ui';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
|
||||
@@ -11,9 +11,10 @@ interface Props {
|
||||
text: string;
|
||||
tags: VariableTag[];
|
||||
loading: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const VariableLink: FC<Props> = ({ loading, onClick: propsOnClick, tags, text }) => {
|
||||
export const VariableLink: FC<Props> = ({ loading, onClick: propsOnClick, tags, text, onCancel }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
const onClick = useCallback(
|
||||
(event: MouseEvent<HTMLAnchorElement>) => {
|
||||
@@ -32,7 +33,7 @@ export const VariableLink: FC<Props> = ({ loading, onClick: propsOnClick, tags,
|
||||
title={text}
|
||||
>
|
||||
<VariableLinkText tags={tags} text={text} />
|
||||
<Icon className="spin-clockwise" name="sync" size="xs" />
|
||||
<LoadingIndicator onCancel={onCancel} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -71,6 +72,22 @@ const VariableLinkText: FC<Pick<Props, 'tags' | 'text'>> = ({ tags, text }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const LoadingIndicator: FC<Pick<Props, 'onCancel'>> = ({ onCancel }) => {
|
||||
const onClick = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
onCancel();
|
||||
},
|
||||
[onCancel]
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip content="Cancel query">
|
||||
<Icon className="spin-clockwise" name="sync" size="xs" onClick={onClick} />
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
container: css`
|
||||
max-width: 500px;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { initialQueryVariableModelState } from './reducer';
|
||||
import { initialVariableEditorState } from '../editor/reducer';
|
||||
import { describe, expect } from '../../../../test/lib/common';
|
||||
import { NEW_VARIABLE_ID } from '../state/types';
|
||||
import DefaultVariableQueryEditor from '../editor/DefaultVariableQueryEditor';
|
||||
import { LegacyVariableQueryEditor } from '../editor/LegacyVariableQueryEditor';
|
||||
|
||||
const setupTestContext = (options: Partial<Props>) => {
|
||||
const defaults: Props = {
|
||||
@@ -20,7 +20,7 @@ const setupTestContext = (options: Partial<Props>) => {
|
||||
editor: {
|
||||
...initialVariableEditorState,
|
||||
extended: {
|
||||
VariableQueryEditor: DefaultVariableQueryEditor,
|
||||
VariableQueryEditor: LegacyVariableQueryEditor,
|
||||
dataSources: [],
|
||||
dataSource: ({} as unknown) as DataSourceApi,
|
||||
},
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import React, { ChangeEvent, PureComponent } from 'react';
|
||||
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||
import { InlineFormLabel, LegacyForms } from '@grafana/ui';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { LoadingState } from '@grafana/data';
|
||||
|
||||
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
|
||||
import { QueryVariableModel, VariableRefresh, VariableSort, VariableWithMultiSupport } from '../types';
|
||||
import { QueryVariableEditorState } from './reducer';
|
||||
import { changeQueryVariableDataSource, changeQueryVariableQuery, initQueryVariableEditor } from './actions';
|
||||
import { VariableEditorState } from '../editor/reducer';
|
||||
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
|
||||
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||
import { StoreState } from '../../../types';
|
||||
import { connectWithStore } from '../../../core/utils/connectWithReduxStore';
|
||||
import { toVariableIdentifier } from '../state/types';
|
||||
import { changeVariableMultiValue } from '../state/actions';
|
||||
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
import { isLegacyQueryEditor, isQueryEditor } from '../guard';
|
||||
|
||||
const { Switch } = LegacyForms;
|
||||
|
||||
@@ -72,12 +75,24 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
|
||||
this.props.onPropChange({ propName: 'datasource', propValue: event.target.value });
|
||||
};
|
||||
|
||||
onQueryChange = async (query: any, definition: string) => {
|
||||
onLegacyQueryChange = async (query: any, definition: string) => {
|
||||
if (this.props.variable.query !== query) {
|
||||
this.props.changeQueryVariableQuery(toVariableIdentifier(this.props.variable), query, definition);
|
||||
}
|
||||
};
|
||||
|
||||
onQueryChange = async (query: any) => {
|
||||
if (this.props.variable.query !== query) {
|
||||
let definition = '';
|
||||
|
||||
if (query && query.hasOwnProperty('query') && typeof query.query === 'string') {
|
||||
definition = query.query;
|
||||
}
|
||||
|
||||
this.props.changeQueryVariableQuery(toVariableIdentifier(this.props.variable), query, definition);
|
||||
}
|
||||
};
|
||||
|
||||
onRegExChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ regex: event.target.value });
|
||||
};
|
||||
@@ -127,8 +142,48 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
|
||||
this.props.onPropChange({ propName: 'useTags', propValue: event.target.checked, updateOptions: true });
|
||||
};
|
||||
|
||||
renderQueryEditor = () => {
|
||||
const { editor, variable } = this.props;
|
||||
if (!editor.extended || !editor.extended.dataSource || !editor.extended.VariableQueryEditor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const query = variable.query;
|
||||
const datasource = editor.extended.dataSource;
|
||||
const VariableQueryEditor = editor.extended.VariableQueryEditor;
|
||||
|
||||
if (isLegacyQueryEditor(VariableQueryEditor, datasource)) {
|
||||
return (
|
||||
<VariableQueryEditor
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
templateSrv={getTemplateSrv()}
|
||||
onChange={this.onLegacyQueryChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const range = getTimeSrv().timeRange();
|
||||
|
||||
if (isQueryEditor(VariableQueryEditor, datasource)) {
|
||||
return (
|
||||
<VariableQueryEditor
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
onChange={this.onQueryChange}
|
||||
onRunQuery={() => {}}
|
||||
data={{ series: [], state: LoadingState.Done, timeRange: range }}
|
||||
range={range}
|
||||
onBlur={() => {}}
|
||||
history={[]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const VariableQueryEditor = this.props.editor.extended?.VariableQueryEditor;
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form-group">
|
||||
@@ -181,14 +236,7 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{VariableQueryEditor && this.props.editor.extended?.dataSource && (
|
||||
<VariableQueryEditor
|
||||
datasource={this.props.editor.extended?.dataSource}
|
||||
query={this.props.variable.query}
|
||||
templateSrv={getTemplateSrv()}
|
||||
onChange={this.onQueryChange}
|
||||
/>
|
||||
)}
|
||||
{this.renderQueryEditor()}
|
||||
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel
|
||||
|
||||
396
public/app/features/variables/query/VariableQueryRunner.test.ts
Normal file
396
public/app/features/variables/query/VariableQueryRunner.test.ts
Normal file
@@ -0,0 +1,396 @@
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { DefaultTimeRange, LoadingState, VariableSupportType } from '@grafana/data';
|
||||
import { delay } from 'rxjs/operators';
|
||||
|
||||
import { UpdateOptionsResults, VariableQueryRunner } from './VariableQueryRunner';
|
||||
import { queryBuilder } from '../shared/testing/builders';
|
||||
import { QueryRunner, QueryRunners } from './queryRunners';
|
||||
import { toVariableIdentifier, VariableIdentifier } from '../state/types';
|
||||
import { QueryVariableModel } from '../types';
|
||||
import { updateVariableOptions, updateVariableTags } from './reducer';
|
||||
|
||||
type DoneCallback = {
|
||||
(...args: any[]): any;
|
||||
fail(error?: string | { message: string }): any;
|
||||
};
|
||||
|
||||
function expectOnResults(args: {
|
||||
runner: VariableQueryRunner;
|
||||
identifier: VariableIdentifier;
|
||||
done: DoneCallback;
|
||||
expect: (results: UpdateOptionsResults[]) => void;
|
||||
}) {
|
||||
const { runner, identifier, done, expect: expectCallback } = args;
|
||||
const results: UpdateOptionsResults[] = [];
|
||||
const subscription = runner.getResponse(identifier).subscribe({
|
||||
next: value => {
|
||||
results.push(value);
|
||||
if (value.state === LoadingState.Done || value.state === LoadingState.Error) {
|
||||
try {
|
||||
expectCallback(results);
|
||||
subscription.unsubscribe();
|
||||
done();
|
||||
} catch (err) {
|
||||
subscription.unsubscribe();
|
||||
done.fail(err);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getTestContext(variable?: QueryVariableModel) {
|
||||
variable =
|
||||
variable ??
|
||||
queryBuilder()
|
||||
.withId('query')
|
||||
.build();
|
||||
const getTimeSrv = jest.fn().mockReturnValue({
|
||||
timeRange: jest.fn().mockReturnValue(DefaultTimeRange),
|
||||
});
|
||||
const datasource: any = { metricFindQuery: jest.fn().mockResolvedValue([]) };
|
||||
const identifier = toVariableIdentifier(variable);
|
||||
const searchFilter = undefined;
|
||||
const getTemplatedRegex = jest.fn().mockReturnValue('getTemplatedRegex result');
|
||||
const dispatch = jest.fn().mockResolvedValue({});
|
||||
const getState = jest.fn().mockReturnValue({
|
||||
templating: {
|
||||
transaction: {
|
||||
uid: '0123456789',
|
||||
},
|
||||
},
|
||||
variables: {
|
||||
[variable.id]: variable,
|
||||
},
|
||||
});
|
||||
const queryRunner: QueryRunner = {
|
||||
type: VariableSupportType.Standard,
|
||||
canRun: jest.fn().mockReturnValue(true),
|
||||
getTarget: jest.fn().mockReturnValue({ refId: 'A', query: 'A query' }),
|
||||
runRequest: jest.fn().mockReturnValue(of({ series: [], state: LoadingState.Done })),
|
||||
};
|
||||
const queryRunners = ({
|
||||
getRunnerForDatasource: jest.fn().mockReturnValue(queryRunner),
|
||||
} as unknown) as QueryRunners;
|
||||
const getVariable = jest.fn().mockReturnValue(variable);
|
||||
const runRequest = jest.fn().mockReturnValue(of({}));
|
||||
const runner = new VariableQueryRunner({
|
||||
getTimeSrv,
|
||||
getTemplatedRegex,
|
||||
dispatch,
|
||||
getState,
|
||||
getVariable,
|
||||
queryRunners,
|
||||
runRequest,
|
||||
});
|
||||
|
||||
return {
|
||||
identifier,
|
||||
datasource,
|
||||
runner,
|
||||
searchFilter,
|
||||
getTemplatedRegex,
|
||||
dispatch,
|
||||
getState,
|
||||
queryRunner,
|
||||
queryRunners,
|
||||
getVariable,
|
||||
runRequest,
|
||||
variable,
|
||||
getTimeSrv,
|
||||
};
|
||||
}
|
||||
|
||||
describe('VariableQueryRunner', () => {
|
||||
describe('happy case', () => {
|
||||
it('then it should work as expected', done => {
|
||||
const {
|
||||
identifier,
|
||||
runner,
|
||||
datasource,
|
||||
getState,
|
||||
getVariable,
|
||||
queryRunners,
|
||||
queryRunner,
|
||||
dispatch,
|
||||
} = getTestContext();
|
||||
|
||||
expectOnResults({
|
||||
identifier,
|
||||
runner,
|
||||
expect: results => {
|
||||
// verify that the observable works as expected
|
||||
expect(results).toEqual([
|
||||
{ state: LoadingState.Loading, identifier },
|
||||
{ state: LoadingState.Done, identifier },
|
||||
]);
|
||||
|
||||
// verify that mocks have been called as expected
|
||||
expect(getState).toHaveBeenCalledTimes(3);
|
||||
expect(getVariable).toHaveBeenCalledTimes(1);
|
||||
expect(queryRunners.getRunnerForDatasource).toHaveBeenCalledTimes(1);
|
||||
expect(queryRunner.getTarget).toHaveBeenCalledTimes(1);
|
||||
expect(queryRunner.runRequest).toHaveBeenCalledTimes(1);
|
||||
expect(datasource.metricFindQuery).not.toHaveBeenCalled();
|
||||
|
||||
// updateVariableOptions and validateVariableSelectionState
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch.mock.calls[0][0]).toEqual(
|
||||
updateVariableOptions({
|
||||
id: 'query',
|
||||
type: 'query',
|
||||
data: { results: [], templatedRegex: 'getTemplatedRegex result' },
|
||||
})
|
||||
);
|
||||
},
|
||||
done,
|
||||
});
|
||||
|
||||
runner.queueRequest({ identifier, datasource });
|
||||
});
|
||||
});
|
||||
|
||||
describe('tags case', () => {
|
||||
it('then it should work as expected', done => {
|
||||
const variable = queryBuilder()
|
||||
.withId('query')
|
||||
.withTags(true)
|
||||
.withTagsQuery('A tags query')
|
||||
.build();
|
||||
const {
|
||||
identifier,
|
||||
runner,
|
||||
datasource,
|
||||
getState,
|
||||
getVariable,
|
||||
queryRunners,
|
||||
queryRunner,
|
||||
dispatch,
|
||||
} = getTestContext(variable);
|
||||
|
||||
expectOnResults({
|
||||
identifier,
|
||||
runner,
|
||||
expect: results => {
|
||||
// verify that the observable works as expected
|
||||
expect(results).toEqual([
|
||||
{ state: LoadingState.Loading, identifier },
|
||||
{ state: LoadingState.Done, identifier },
|
||||
]);
|
||||
|
||||
// verify that mocks have been called as expected
|
||||
expect(getState).toHaveBeenCalledTimes(3);
|
||||
expect(getVariable).toHaveBeenCalledTimes(1);
|
||||
expect(queryRunners.getRunnerForDatasource).toHaveBeenCalledTimes(1);
|
||||
expect(queryRunner.getTarget).toHaveBeenCalledTimes(1);
|
||||
expect(queryRunner.runRequest).toHaveBeenCalledTimes(1);
|
||||
expect(datasource.metricFindQuery).toHaveBeenCalledTimes(1);
|
||||
|
||||
// updateVariableOptions, updateVariableTags and validateVariableSelectionState
|
||||
expect(dispatch).toHaveBeenCalledTimes(3);
|
||||
expect(dispatch.mock.calls[0][0]).toEqual(
|
||||
updateVariableOptions({
|
||||
id: 'query',
|
||||
type: 'query',
|
||||
data: { results: [], templatedRegex: 'getTemplatedRegex result' },
|
||||
})
|
||||
);
|
||||
expect(dispatch.mock.calls[1][0]).toEqual(updateVariableTags({ id: 'query', type: 'query', data: [] }));
|
||||
},
|
||||
done,
|
||||
});
|
||||
|
||||
runner.queueRequest({ identifier, datasource });
|
||||
});
|
||||
});
|
||||
|
||||
describe('error cases', () => {
|
||||
describe('queryRunners.getRunnerForDatasource throws', () => {
|
||||
it('then it should work as expected', done => {
|
||||
const {
|
||||
identifier,
|
||||
runner,
|
||||
datasource,
|
||||
getState,
|
||||
getVariable,
|
||||
queryRunners,
|
||||
queryRunner,
|
||||
dispatch,
|
||||
} = getTestContext();
|
||||
|
||||
queryRunners.getRunnerForDatasource = jest.fn().mockImplementation(() => {
|
||||
throw new Error('getRunnerForDatasource error');
|
||||
});
|
||||
|
||||
expectOnResults({
|
||||
identifier,
|
||||
runner,
|
||||
expect: results => {
|
||||
// verify that the observable works as expected
|
||||
expect(results).toEqual([
|
||||
{ state: LoadingState.Loading, identifier },
|
||||
{ state: LoadingState.Error, identifier, error: new Error('getRunnerForDatasource error') },
|
||||
]);
|
||||
|
||||
// verify that mocks have been called as expected
|
||||
expect(getState).toHaveBeenCalledTimes(2);
|
||||
expect(getVariable).toHaveBeenCalledTimes(1);
|
||||
expect(queryRunners.getRunnerForDatasource).toHaveBeenCalledTimes(1);
|
||||
expect(queryRunner.getTarget).not.toHaveBeenCalled();
|
||||
expect(queryRunner.runRequest).not.toHaveBeenCalled();
|
||||
expect(datasource.metricFindQuery).not.toHaveBeenCalled();
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
},
|
||||
done,
|
||||
});
|
||||
|
||||
runner.queueRequest({ identifier, datasource });
|
||||
});
|
||||
});
|
||||
|
||||
describe('runRequest throws', () => {
|
||||
it('then it should work as expected', done => {
|
||||
const {
|
||||
identifier,
|
||||
runner,
|
||||
datasource,
|
||||
getState,
|
||||
getVariable,
|
||||
queryRunners,
|
||||
queryRunner,
|
||||
dispatch,
|
||||
} = getTestContext();
|
||||
|
||||
queryRunner.runRequest = jest.fn().mockReturnValue(throwError(new Error('runRequest error')));
|
||||
|
||||
expectOnResults({
|
||||
identifier,
|
||||
runner,
|
||||
expect: results => {
|
||||
// verify that the observable works as expected
|
||||
expect(results).toEqual([
|
||||
{ state: LoadingState.Loading, identifier },
|
||||
{ state: LoadingState.Error, identifier, error: new Error('runRequest error') },
|
||||
]);
|
||||
|
||||
// verify that mocks have been called as expected
|
||||
expect(getState).toHaveBeenCalledTimes(2);
|
||||
expect(getVariable).toHaveBeenCalledTimes(1);
|
||||
expect(queryRunners.getRunnerForDatasource).toHaveBeenCalledTimes(1);
|
||||
expect(queryRunner.getTarget).toHaveBeenCalledTimes(1);
|
||||
expect(queryRunner.runRequest).toHaveBeenCalledTimes(1);
|
||||
expect(datasource.metricFindQuery).not.toHaveBeenCalled();
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
},
|
||||
done,
|
||||
});
|
||||
|
||||
runner.queueRequest({ identifier, datasource });
|
||||
});
|
||||
});
|
||||
|
||||
describe('metricFindQuery throws', () => {
|
||||
it('then it should work as expected', done => {
|
||||
const variable = queryBuilder()
|
||||
.withId('query')
|
||||
.withTags(true)
|
||||
.withTagsQuery('A tags query')
|
||||
.build();
|
||||
const {
|
||||
identifier,
|
||||
runner,
|
||||
datasource,
|
||||
getState,
|
||||
getVariable,
|
||||
queryRunners,
|
||||
queryRunner,
|
||||
dispatch,
|
||||
} = getTestContext(variable);
|
||||
|
||||
datasource.metricFindQuery = jest.fn().mockRejectedValue(new Error('metricFindQuery error'));
|
||||
|
||||
expectOnResults({
|
||||
identifier,
|
||||
runner,
|
||||
expect: results => {
|
||||
// verify that the observable works as expected
|
||||
expect(results).toEqual([
|
||||
{ state: LoadingState.Loading, identifier },
|
||||
{ state: LoadingState.Error, identifier, error: new Error('metricFindQuery error') },
|
||||
]);
|
||||
|
||||
// verify that mocks have been called as expected
|
||||
expect(getState).toHaveBeenCalledTimes(3);
|
||||
expect(getVariable).toHaveBeenCalledTimes(1);
|
||||
expect(queryRunners.getRunnerForDatasource).toHaveBeenCalledTimes(1);
|
||||
expect(queryRunner.getTarget).toHaveBeenCalledTimes(1);
|
||||
expect(queryRunner.runRequest).toHaveBeenCalledTimes(1);
|
||||
expect(datasource.metricFindQuery).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
done,
|
||||
});
|
||||
|
||||
runner.queueRequest({ identifier, datasource });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancellation cases', () => {
|
||||
describe('long running request is cancelled', () => {
|
||||
it('then it should work as expected', done => {
|
||||
const { identifier, datasource, runner, queryRunner } = getTestContext();
|
||||
|
||||
queryRunner.runRequest = jest
|
||||
.fn()
|
||||
.mockReturnValue(of({ series: [], state: LoadingState.Done }).pipe(delay(10000)));
|
||||
|
||||
expectOnResults({
|
||||
identifier,
|
||||
runner,
|
||||
expect: results => {
|
||||
// verify that the observable works as expected
|
||||
expect(results).toEqual([
|
||||
{ state: LoadingState.Loading, identifier },
|
||||
{ state: LoadingState.Loading, identifier, cancelled: true },
|
||||
{ state: LoadingState.Done, identifier },
|
||||
]);
|
||||
},
|
||||
done,
|
||||
});
|
||||
|
||||
runner.queueRequest({ identifier, datasource });
|
||||
runner.cancelRequest(identifier);
|
||||
});
|
||||
});
|
||||
|
||||
describe('an identical request is triggered before first request is finished', () => {
|
||||
it('then it should work as expected', done => {
|
||||
const { identifier, datasource, runner, queryRunner } = getTestContext();
|
||||
|
||||
queryRunner.runRequest = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(of({ series: [], state: LoadingState.Done }).pipe(delay(10000)))
|
||||
.mockReturnValue(of({ series: [], state: LoadingState.Done }));
|
||||
|
||||
expectOnResults({
|
||||
identifier,
|
||||
runner,
|
||||
expect: results => {
|
||||
// verify that the observable works as expected
|
||||
expect(results).toEqual([
|
||||
{ state: LoadingState.Loading, identifier },
|
||||
{ state: LoadingState.Loading, identifier },
|
||||
{ state: LoadingState.Loading, identifier, cancelled: true },
|
||||
{ state: LoadingState.Done, identifier },
|
||||
]);
|
||||
},
|
||||
done,
|
||||
});
|
||||
|
||||
runner.queueRequest({ identifier, datasource });
|
||||
runner.queueRequest({ identifier, datasource });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
206
public/app/features/variables/query/VariableQueryRunner.ts
Normal file
206
public/app/features/variables/query/VariableQueryRunner.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { merge, Observable, of, Subject, throwError, Unsubscribable } from 'rxjs';
|
||||
import { catchError, filter, finalize, first, mergeMap, takeUntil } from 'rxjs/operators';
|
||||
import {
|
||||
CoreApp,
|
||||
DataQuery,
|
||||
DataQueryRequest,
|
||||
DataSourceApi,
|
||||
DefaultTimeRange,
|
||||
LoadingState,
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { VariableIdentifier } from '../state/types';
|
||||
import { getVariable } from '../state/selectors';
|
||||
import { QueryVariableModel, VariableRefresh } from '../types';
|
||||
import { StoreState, ThunkDispatch } from '../../../types';
|
||||
import { dispatch, getState } from '../../../store/store';
|
||||
import { getTemplatedRegex } from '../utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
import { QueryRunners } from './queryRunners';
|
||||
import { runRequest } from '../../dashboard/state/runRequest';
|
||||
import {
|
||||
runUpdateTagsRequest,
|
||||
toMetricFindValues,
|
||||
updateOptionsState,
|
||||
updateTagsState,
|
||||
validateVariableSelection,
|
||||
} from './operators';
|
||||
|
||||
interface UpdateOptionsArgs {
|
||||
identifier: VariableIdentifier;
|
||||
datasource: DataSourceApi;
|
||||
searchFilter?: string;
|
||||
}
|
||||
|
||||
export interface UpdateOptionsResults {
|
||||
state: LoadingState;
|
||||
identifier: VariableIdentifier;
|
||||
error?: any;
|
||||
cancelled?: boolean;
|
||||
}
|
||||
|
||||
interface VariableQueryRunnerArgs {
|
||||
dispatch: ThunkDispatch;
|
||||
getState: () => StoreState;
|
||||
getVariable: typeof getVariable;
|
||||
getTemplatedRegex: typeof getTemplatedRegex;
|
||||
getTimeSrv: typeof getTimeSrv;
|
||||
queryRunners: QueryRunners;
|
||||
runRequest: typeof runRequest;
|
||||
}
|
||||
|
||||
export class VariableQueryRunner {
|
||||
private readonly updateOptionsRequests: Subject<UpdateOptionsArgs>;
|
||||
private readonly updateOptionsResults: Subject<UpdateOptionsResults>;
|
||||
private readonly cancelRequests: Subject<{ identifier: VariableIdentifier }>;
|
||||
private readonly subscription: Unsubscribable;
|
||||
|
||||
constructor(
|
||||
private dependencies: VariableQueryRunnerArgs = {
|
||||
dispatch,
|
||||
getState,
|
||||
getVariable,
|
||||
getTemplatedRegex,
|
||||
getTimeSrv,
|
||||
queryRunners: new QueryRunners(),
|
||||
runRequest,
|
||||
}
|
||||
) {
|
||||
this.updateOptionsRequests = new Subject<UpdateOptionsArgs>();
|
||||
this.updateOptionsResults = new Subject<UpdateOptionsResults>();
|
||||
this.cancelRequests = new Subject<{ identifier: VariableIdentifier }>();
|
||||
this.onNewRequest = this.onNewRequest.bind(this);
|
||||
this.subscription = this.updateOptionsRequests.subscribe(this.onNewRequest);
|
||||
}
|
||||
|
||||
queueRequest(args: UpdateOptionsArgs): void {
|
||||
this.updateOptionsRequests.next(args);
|
||||
}
|
||||
|
||||
getResponse(identifier: VariableIdentifier): Observable<UpdateOptionsResults> {
|
||||
return this.updateOptionsResults.asObservable().pipe(filter(result => result.identifier === identifier));
|
||||
}
|
||||
|
||||
cancelRequest(identifier: VariableIdentifier): void {
|
||||
this.cancelRequests.next({ identifier });
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
private onNewRequest(args: UpdateOptionsArgs): void {
|
||||
const { datasource, identifier, searchFilter } = args;
|
||||
try {
|
||||
const {
|
||||
dispatch,
|
||||
runRequest,
|
||||
getTemplatedRegex: getTemplatedRegexFunc,
|
||||
getVariable,
|
||||
queryRunners,
|
||||
getTimeSrv,
|
||||
getState,
|
||||
} = this.dependencies;
|
||||
|
||||
const beforeUid = getState().templating.transaction.uid;
|
||||
|
||||
this.updateOptionsResults.next({ identifier, state: LoadingState.Loading });
|
||||
|
||||
const variable = getVariable<QueryVariableModel>(identifier.id, getState());
|
||||
const timeSrv = getTimeSrv();
|
||||
const runnerArgs = { variable, datasource, searchFilter, timeSrv, runRequest };
|
||||
const runner = queryRunners.getRunnerForDatasource(datasource);
|
||||
const target = runner.getTarget({ datasource, variable });
|
||||
const request = this.getRequest(variable, args, target);
|
||||
|
||||
runner
|
||||
.runRequest(runnerArgs, request)
|
||||
.pipe(
|
||||
filter(() => {
|
||||
// lets check if we started another batch during the execution of the observable. If so we just want to abort the rest.
|
||||
const afterUid = getState().templating.transaction.uid;
|
||||
return beforeUid === afterUid;
|
||||
}),
|
||||
first(data => data.state === LoadingState.Done || data.state === LoadingState.Error),
|
||||
mergeMap(data => {
|
||||
if (data.state === LoadingState.Error) {
|
||||
return throwError(data.error);
|
||||
}
|
||||
|
||||
return of(data);
|
||||
}),
|
||||
toMetricFindValues(),
|
||||
updateOptionsState({ variable, dispatch, getTemplatedRegexFunc }),
|
||||
runUpdateTagsRequest({ variable, datasource, searchFilter }),
|
||||
updateTagsState({ variable, dispatch }),
|
||||
validateVariableSelection({ variable, dispatch, searchFilter }),
|
||||
takeUntil(
|
||||
merge(this.updateOptionsRequests, this.cancelRequests).pipe(
|
||||
filter(args => {
|
||||
let cancelRequest = false;
|
||||
|
||||
if (args.identifier.id === identifier.id) {
|
||||
cancelRequest = true;
|
||||
this.updateOptionsResults.next({ identifier, state: LoadingState.Loading, cancelled: cancelRequest });
|
||||
}
|
||||
|
||||
return cancelRequest;
|
||||
})
|
||||
)
|
||||
),
|
||||
catchError(error => {
|
||||
if (error.cancelled) {
|
||||
return of({});
|
||||
}
|
||||
|
||||
this.updateOptionsResults.next({ identifier, state: LoadingState.Error, error });
|
||||
return throwError(error);
|
||||
}),
|
||||
finalize(() => {
|
||||
this.updateOptionsResults.next({ identifier, state: LoadingState.Done });
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
} catch (error) {
|
||||
this.updateOptionsResults.next({ identifier, state: LoadingState.Error, error });
|
||||
}
|
||||
}
|
||||
|
||||
private getRequest(variable: QueryVariableModel, args: UpdateOptionsArgs, target: DataQuery) {
|
||||
const { searchFilter } = args;
|
||||
const variableAsVars = { variable: { text: variable.current.text, value: variable.current.value } };
|
||||
const searchFilterScope = { searchFilter: { text: searchFilter, value: searchFilter } };
|
||||
const searchFilterAsVars = searchFilter ? searchFilterScope : {};
|
||||
const scopedVars = { ...searchFilterAsVars, ...variableAsVars } as ScopedVars;
|
||||
const range =
|
||||
variable.refresh === VariableRefresh.onTimeRangeChanged
|
||||
? this.dependencies.getTimeSrv().timeRange()
|
||||
: DefaultTimeRange;
|
||||
|
||||
const request: DataQueryRequest = {
|
||||
app: CoreApp.Dashboard,
|
||||
requestId: uuidv4(),
|
||||
timezone: '',
|
||||
range,
|
||||
interval: '',
|
||||
intervalMs: 0,
|
||||
targets: [target],
|
||||
scopedVars,
|
||||
startTime: Date.now(),
|
||||
};
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
let singleton: VariableQueryRunner;
|
||||
|
||||
export function setVariableQueryRunner(runner: VariableQueryRunner): void {
|
||||
singleton = runner;
|
||||
}
|
||||
|
||||
export function getVariableQueryRunner(): VariableQueryRunner {
|
||||
return singleton;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LoadingState } from '@grafana/data';
|
||||
import { DefaultTimeRange, LoadingState } from '@grafana/data';
|
||||
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { createQueryVariableAdapter } from './adapter';
|
||||
@@ -18,6 +18,8 @@ import { TemplatingState } from '../state/reducers';
|
||||
import {
|
||||
changeQueryVariableDataSource,
|
||||
changeQueryVariableQuery,
|
||||
flattenQuery,
|
||||
hasSelfReferencingQuery,
|
||||
initQueryVariableEditor,
|
||||
updateQueryVariableOptions,
|
||||
} from './actions';
|
||||
@@ -28,11 +30,13 @@ import {
|
||||
removeVariableEditorError,
|
||||
setIdInEditor,
|
||||
} from '../editor/reducer';
|
||||
import DefaultVariableQueryEditor from '../editor/DefaultVariableQueryEditor';
|
||||
import { LegacyVariableQueryEditor } from '../editor/LegacyVariableQueryEditor';
|
||||
import { expect } from 'test/lib/common';
|
||||
import { updateOptions } from '../state/actions';
|
||||
import { notifyApp } from '../../../core/reducers/appNotification';
|
||||
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
|
||||
import { getTimeSrv, setTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
import { setVariableQueryRunner, VariableQueryRunner } from './VariableQueryRunner';
|
||||
|
||||
const mocks: Record<string, any> = {
|
||||
datasource: {
|
||||
@@ -62,6 +66,20 @@ jest.mock('../../templating/template_srv', () => ({
|
||||
}));
|
||||
|
||||
describe('query actions', () => {
|
||||
let originalTimeSrv: TimeSrv;
|
||||
|
||||
beforeEach(() => {
|
||||
originalTimeSrv = getTimeSrv();
|
||||
setTimeSrv(({
|
||||
timeRange: jest.fn().mockReturnValue(DefaultTimeRange),
|
||||
} as unknown) as TimeSrv);
|
||||
setVariableQueryRunner(new VariableQueryRunner());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setTimeSrv(originalTimeSrv);
|
||||
});
|
||||
|
||||
variableAdapters.setInit(() => [createQueryVariableAdapter()]);
|
||||
|
||||
describe('when updateQueryVariableOptions is dispatched for variable with tags and includeAll', () => {
|
||||
@@ -80,15 +98,11 @@ describe('query actions', () => {
|
||||
const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
|
||||
const update = { results: optionsMetrics, templatedRegex: '' };
|
||||
|
||||
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
|
||||
const [updateOptions, updateTags, setCurrentAction] = actions;
|
||||
const expectedNumberOfActions = 3;
|
||||
|
||||
expect(updateOptions).toEqual(updateVariableOptions(toVariablePayload(variable, update)));
|
||||
expect(updateTags).toEqual(updateVariableTags(toVariablePayload(variable, tagsMetrics)));
|
||||
expect(setCurrentAction).toEqual(setCurrentVariableValue(toVariablePayload(variable, { option })));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
updateVariableOptions(toVariablePayload(variable, update)),
|
||||
updateVariableTags(toVariablePayload(variable, tagsMetrics)),
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option }))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -135,14 +149,10 @@ describe('query actions', () => {
|
||||
const option = createOption('A');
|
||||
const update = { results: optionsMetrics, templatedRegex: '' };
|
||||
|
||||
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
|
||||
const [updateOptions, setCurrentAction] = actions;
|
||||
const expectedNumberOfActions = 2;
|
||||
|
||||
expect(updateOptions).toEqual(updateVariableOptions(toVariablePayload(variable, update)));
|
||||
expect(setCurrentAction).toEqual(setCurrentVariableValue(toVariablePayload(variable, { option })));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
updateVariableOptions(toVariablePayload(variable, update)),
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option }))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -410,7 +420,7 @@ describe('query actions', () => {
|
||||
describe('when changeQueryVariableDataSource is dispatched and editor is not configured', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = createVariable({ datasource: 'other' });
|
||||
const editor = DefaultVariableQueryEditor;
|
||||
const editor = LegacyVariableQueryEditor;
|
||||
|
||||
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
|
||||
components: {},
|
||||
@@ -550,6 +560,162 @@ describe('query actions', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasSelfReferencingQuery', () => {
|
||||
it('when called with a string', () => {
|
||||
const query = '$query';
|
||||
const name = 'query';
|
||||
|
||||
expect(hasSelfReferencingQuery(name, query)).toBe(true);
|
||||
});
|
||||
|
||||
it('when called with an array', () => {
|
||||
const query = ['$query'];
|
||||
const name = 'query';
|
||||
|
||||
expect(hasSelfReferencingQuery(name, query)).toBe(true);
|
||||
});
|
||||
|
||||
it('when called with a simple object', () => {
|
||||
const query = { a: '$query' };
|
||||
const name = 'query';
|
||||
|
||||
expect(hasSelfReferencingQuery(name, query)).toBe(true);
|
||||
});
|
||||
|
||||
it('when called with a complex object', () => {
|
||||
const query = {
|
||||
level2: {
|
||||
level3: {
|
||||
query: 'query3',
|
||||
refId: 'C',
|
||||
num: 2,
|
||||
bool: true,
|
||||
arr: [
|
||||
{ query: 'query4', refId: 'D', num: 4, bool: true },
|
||||
{
|
||||
query: 'query5',
|
||||
refId: 'E',
|
||||
num: 5,
|
||||
bool: true,
|
||||
arr: [{ query: '$query', refId: 'F', num: 6, bool: true }],
|
||||
},
|
||||
],
|
||||
},
|
||||
query: 'query2',
|
||||
refId: 'B',
|
||||
num: 1,
|
||||
bool: false,
|
||||
},
|
||||
query: 'query1',
|
||||
refId: 'A',
|
||||
num: 0,
|
||||
bool: true,
|
||||
arr: [
|
||||
{ query: 'query7', refId: 'G', num: 7, bool: true },
|
||||
{
|
||||
query: 'query8',
|
||||
refId: 'H',
|
||||
num: 8,
|
||||
bool: true,
|
||||
arr: [{ query: 'query9', refId: 'I', num: 9, bool: true }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const name = 'query';
|
||||
|
||||
expect(hasSelfReferencingQuery(name, query)).toBe(true);
|
||||
});
|
||||
|
||||
it('when called with a number', () => {
|
||||
const query = 1;
|
||||
const name = 'query';
|
||||
|
||||
expect(hasSelfReferencingQuery(name, query)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flattenQuery', () => {
|
||||
it('when called with a complex object', () => {
|
||||
const query = {
|
||||
level2: {
|
||||
level3: {
|
||||
query: '${query3}',
|
||||
refId: 'C',
|
||||
num: 2,
|
||||
bool: true,
|
||||
arr: [
|
||||
{ query: '${query4}', refId: 'D', num: 4, bool: true },
|
||||
{
|
||||
query: '${query5}',
|
||||
refId: 'E',
|
||||
num: 5,
|
||||
bool: true,
|
||||
arr: [{ query: '${query6}', refId: 'F', num: 6, bool: true }],
|
||||
},
|
||||
],
|
||||
},
|
||||
query: '${query2}',
|
||||
refId: 'B',
|
||||
num: 1,
|
||||
bool: false,
|
||||
},
|
||||
query: '${query1}',
|
||||
refId: 'A',
|
||||
num: 0,
|
||||
bool: true,
|
||||
arr: [
|
||||
{ query: '${query7}', refId: 'G', num: 7, bool: true },
|
||||
{
|
||||
query: '${query8}',
|
||||
refId: 'H',
|
||||
num: 8,
|
||||
bool: true,
|
||||
arr: [{ query: '${query9}', refId: 'I', num: 9, bool: true }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(flattenQuery(query)).toEqual({
|
||||
query: '${query1}',
|
||||
refId: 'A',
|
||||
num: 0,
|
||||
bool: true,
|
||||
level2_query: '${query2}',
|
||||
level2_refId: 'B',
|
||||
level2_num: 1,
|
||||
level2_bool: false,
|
||||
level2_level3_query: '${query3}',
|
||||
level2_level3_refId: 'C',
|
||||
level2_level3_num: 2,
|
||||
level2_level3_bool: true,
|
||||
level2_level3_arr_0_query: '${query4}',
|
||||
level2_level3_arr_0_refId: 'D',
|
||||
level2_level3_arr_0_num: 4,
|
||||
level2_level3_arr_0_bool: true,
|
||||
level2_level3_arr_1_query: '${query5}',
|
||||
level2_level3_arr_1_refId: 'E',
|
||||
level2_level3_arr_1_num: 5,
|
||||
level2_level3_arr_1_bool: true,
|
||||
level2_level3_arr_1_arr_0_query: '${query6}',
|
||||
level2_level3_arr_1_arr_0_refId: 'F',
|
||||
level2_level3_arr_1_arr_0_num: 6,
|
||||
level2_level3_arr_1_arr_0_bool: true,
|
||||
arr_0_query: '${query7}',
|
||||
arr_0_refId: 'G',
|
||||
arr_0_num: 7,
|
||||
arr_0_bool: true,
|
||||
arr_1_query: '${query8}',
|
||||
arr_1_refId: 'H',
|
||||
arr_1_num: 8,
|
||||
arr_1_bool: true,
|
||||
arr_1_arr_0_query: '${query9}',
|
||||
arr_1_arr_0_refId: 'I',
|
||||
arr_1_arr_0_num: 9,
|
||||
arr_1_arr_0_bool: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockDatasourceMetrics(variable: QueryVariableModel, optionsMetrics: any[], tagsMetrics: any[]) {
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { DataSourcePluginMeta, DataSourceSelectItem } from '@grafana/data';
|
||||
import { toDataQueryError, getTemplateSrv } from '@grafana/runtime';
|
||||
import { DataQuery, DataSourceApi, DataSourcePluginMeta, DataSourceSelectItem } from '@grafana/data';
|
||||
import { toDataQueryError } from '@grafana/runtime';
|
||||
|
||||
import { updateOptions, validateVariableSelectionState } from '../state/actions';
|
||||
import { QueryVariableModel, VariableRefresh } from '../types';
|
||||
import { updateOptions } from '../state/actions';
|
||||
import { QueryVariableModel } from '../types';
|
||||
import { ThunkResult } from '../../../types';
|
||||
import { getDatasourceSrv } from '../../plugins/datasource_srv';
|
||||
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
import { importDataSourcePlugin } from '../../plugins/plugin_loader';
|
||||
import DefaultVariableQueryEditor from '../editor/DefaultVariableQueryEditor';
|
||||
import { getVariable } from '../state/selectors';
|
||||
import { addVariableEditorError, changeVariableEditorExtended, removeVariableEditorError } from '../editor/reducer';
|
||||
import { changeVariableProp } from '../state/sharedReducer';
|
||||
import { updateVariableOptions, updateVariableTags } from './reducer';
|
||||
import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
|
||||
import { hasLegacyVariableSupport, hasStandardVariableSupport } from '../guard';
|
||||
import { getVariableQueryEditor } from '../editor/getVariableQueryEditor';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { getVariableQueryRunner } from './VariableQueryRunner';
|
||||
import { variableQueryObserver } from './variableQueryObserver';
|
||||
|
||||
export const updateQueryVariableOptions = (
|
||||
identifier: VariableIdentifier,
|
||||
@@ -21,43 +22,24 @@ export const updateQueryVariableOptions = (
|
||||
return async (dispatch, getState) => {
|
||||
const variableInState = getVariable<QueryVariableModel>(identifier.id, getState());
|
||||
try {
|
||||
const beforeUid = getState().templating.transaction.uid;
|
||||
if (getState().templating.editor.id === variableInState.id) {
|
||||
dispatch(removeVariableEditorError({ errorProp: 'update' }));
|
||||
}
|
||||
const dataSource = await getDatasourceSrv().get(variableInState.datasource ?? '');
|
||||
const queryOptions: any = { range: undefined, variable: variableInState, searchFilter };
|
||||
if (variableInState.refresh === VariableRefresh.onTimeRangeChanged) {
|
||||
queryOptions.range = getTimeSrv().timeRange();
|
||||
}
|
||||
const datasource = await getDatasourceSrv().get(variableInState.datasource ?? '');
|
||||
dispatch(upgradeLegacyQueries(identifier, datasource));
|
||||
|
||||
if (!dataSource.metricFindQuery) {
|
||||
return;
|
||||
}
|
||||
// we need to await the result from variableQueryRunner before moving on otherwise variables dependent on this
|
||||
// variable will have the wrong current value as input
|
||||
await new Promise((resolve, reject) => {
|
||||
const subscription: Subscription = new Subscription();
|
||||
const observer = variableQueryObserver(resolve, reject, subscription);
|
||||
const responseSubscription = getVariableQueryRunner()
|
||||
.getResponse(identifier)
|
||||
.subscribe(observer);
|
||||
subscription.add(responseSubscription);
|
||||
|
||||
const results = await dataSource.metricFindQuery(variableInState.query, queryOptions);
|
||||
|
||||
const afterUid = getState().templating.transaction.uid;
|
||||
if (beforeUid !== afterUid) {
|
||||
// we started another batch before this metricFindQuery finished let's abort
|
||||
return;
|
||||
}
|
||||
|
||||
const templatedRegex = getTemplatedRegex(variableInState);
|
||||
await dispatch(updateVariableOptions(toVariablePayload(variableInState, { results, templatedRegex })));
|
||||
|
||||
if (variableInState.useTags) {
|
||||
const tagResults = await dataSource.metricFindQuery(variableInState.tagsQuery, queryOptions);
|
||||
await dispatch(updateVariableTags(toVariablePayload(variableInState, tagResults)));
|
||||
}
|
||||
|
||||
// If we are searching options there is no need to validate selection state
|
||||
// This condition was added to as validateVariableSelectionState will update the current value of the variable
|
||||
// So after search and selection the current value is already update so no setValue, refresh & url update is performed
|
||||
// The if statement below fixes https://github.com/grafana/grafana/issues/25671
|
||||
if (!searchFilter) {
|
||||
await dispatch(validateVariableSelectionState(toVariableIdentifier(variableInState)));
|
||||
}
|
||||
getVariableQueryRunner().queueRequest({ identifier, datasource, searchFilter });
|
||||
});
|
||||
} catch (err) {
|
||||
const error = toDataQueryError(err);
|
||||
if (getState().templating.editor.id === variableInState.id) {
|
||||
@@ -95,9 +77,9 @@ export const changeQueryVariableDataSource = (
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
const dataSource = await getDatasourceSrv().get(name ?? '');
|
||||
const dsPlugin = await importDataSourcePlugin(dataSource.meta!);
|
||||
const VariableQueryEditor = dsPlugin.components.VariableQueryEditor ?? DefaultVariableQueryEditor;
|
||||
dispatch(changeVariableEditorExtended({ propName: 'dataSource', propValue: dataSource }));
|
||||
|
||||
const VariableQueryEditor = await getVariableQueryEditor(dataSource);
|
||||
dispatch(changeVariableEditorExtended({ propName: 'VariableQueryEditor', propValue: VariableQueryEditor }));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -108,10 +90,10 @@ export const changeQueryVariableDataSource = (
|
||||
export const changeQueryVariableQuery = (
|
||||
identifier: VariableIdentifier,
|
||||
query: any,
|
||||
definition: string
|
||||
definition?: string
|
||||
): ThunkResult<void> => async (dispatch, getState) => {
|
||||
const variableInState = getVariable<QueryVariableModel>(identifier.id, getState());
|
||||
if (typeof query === 'string' && query.match(new RegExp('\\$' + variableInState.name + '(/| |$)'))) {
|
||||
if (hasSelfReferencingQuery(variableInState.name, query)) {
|
||||
const errorText = 'Query cannot contain a reference to itself. Variable: $' + variableInState.name;
|
||||
dispatch(addVariableEditorError({ errorProp: 'query', errorText }));
|
||||
return;
|
||||
@@ -119,18 +101,92 @@ export const changeQueryVariableQuery = (
|
||||
|
||||
dispatch(removeVariableEditorError({ errorProp: 'query' }));
|
||||
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: query })));
|
||||
|
||||
if (definition) {
|
||||
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: definition })));
|
||||
} else if (typeof query === 'string') {
|
||||
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: query })));
|
||||
}
|
||||
|
||||
await dispatch(updateOptions(identifier));
|
||||
};
|
||||
|
||||
const getTemplatedRegex = (variable: QueryVariableModel): string => {
|
||||
if (!variable) {
|
||||
return '';
|
||||
export function hasSelfReferencingQuery(name: string, query: any): boolean {
|
||||
if (typeof query === 'string' && query.match(new RegExp('\\$' + name + '(/| |$)'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!variable.regex) {
|
||||
return '';
|
||||
const flattened = flattenQuery(query);
|
||||
|
||||
for (let prop in flattened) {
|
||||
if (flattened.hasOwnProperty(prop)) {
|
||||
const value = flattened[prop];
|
||||
if (typeof value === 'string' && value.match(new RegExp('\\$' + name + '(/| |$)'))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return getTemplateSrv().replace(variable.regex, {}, 'regex');
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Function that takes any object and flattens all props into one level deep object
|
||||
* */
|
||||
export function flattenQuery(query: any): any {
|
||||
if (typeof query !== 'object') {
|
||||
return { query };
|
||||
}
|
||||
|
||||
const keys = Object.keys(query);
|
||||
const flattened = keys.reduce((all, key) => {
|
||||
const value = query[key];
|
||||
if (typeof value !== 'object') {
|
||||
all[key] = value;
|
||||
return all;
|
||||
}
|
||||
|
||||
const result = flattenQuery(value);
|
||||
for (let childProp in result) {
|
||||
if (result.hasOwnProperty(childProp)) {
|
||||
all[`${key}_${childProp}`] = result[childProp];
|
||||
}
|
||||
}
|
||||
|
||||
return all;
|
||||
}, {} as Record<string, any>);
|
||||
|
||||
return flattened;
|
||||
}
|
||||
|
||||
export function upgradeLegacyQueries(identifier: VariableIdentifier, datasource: DataSourceApi): ThunkResult<void> {
|
||||
return function(dispatch, getState) {
|
||||
if (hasLegacyVariableSupport(datasource)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasStandardVariableSupport(datasource)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const variable = getVariable<QueryVariableModel>(identifier.id, getState());
|
||||
if (isDataQuery(variable.query)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = {
|
||||
refId: `${datasource.name}-${identifier.id}-Variable-Query`,
|
||||
query: variable.query,
|
||||
};
|
||||
|
||||
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: query })));
|
||||
};
|
||||
}
|
||||
|
||||
function isDataQuery(query: any): query is DataQuery {
|
||||
if (!query) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return query.hasOwnProperty('refId') && typeof query.refId === 'string';
|
||||
}
|
||||
|
||||
332
public/app/features/variables/query/operators.test.ts
Normal file
332
public/app/features/variables/query/operators.test.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
import { of } from 'rxjs';
|
||||
import { queryBuilder } from '../shared/testing/builders';
|
||||
import { FieldType, observableTester, toDataFrame } from '@grafana/data';
|
||||
import { initialQueryVariableModelState, updateVariableOptions, updateVariableTags } from './reducer';
|
||||
import { toVariablePayload } from '../state/types';
|
||||
import { VariableRefresh } from '../types';
|
||||
import {
|
||||
areMetricFindValues,
|
||||
runUpdateTagsRequest,
|
||||
toMetricFindValues,
|
||||
updateOptionsState,
|
||||
updateTagsState,
|
||||
validateVariableSelection,
|
||||
} from './operators';
|
||||
|
||||
describe('operators', () => {
|
||||
describe('validateVariableSelection', () => {
|
||||
describe('when called', () => {
|
||||
it('then the correct observable should be created', done => {
|
||||
const variable = queryBuilder()
|
||||
.withId('query')
|
||||
.build();
|
||||
const dispatch = jest.fn().mockResolvedValue({});
|
||||
const observable = of(undefined).pipe(
|
||||
validateVariableSelection({ variable, dispatch, searchFilter: 'A search filter' })
|
||||
);
|
||||
|
||||
observableTester().subscribeAndExpectOnNext({
|
||||
observable,
|
||||
expect: value => {
|
||||
expect(value).toEqual({});
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
done,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTagsState', () => {
|
||||
describe('when called with a variable that uses Tags', () => {
|
||||
it('then the correct observable should be created', done => {
|
||||
const variable = queryBuilder()
|
||||
.withId('query')
|
||||
.withTags(true)
|
||||
.build();
|
||||
const dispatch = jest.fn().mockResolvedValue({});
|
||||
const observable = of([{ text: 'A text' }]).pipe(updateTagsState({ variable, dispatch }));
|
||||
|
||||
observableTester().subscribeAndExpectOnNext({
|
||||
observable,
|
||||
expect: value => {
|
||||
expect(value).toEqual(undefined);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
updateVariableTags(toVariablePayload(variable, [{ text: 'A text' }]))
|
||||
);
|
||||
},
|
||||
done,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a variable that does not use Tags', () => {
|
||||
it('then the correct observable should be created', done => {
|
||||
const variable = queryBuilder()
|
||||
.withId('query')
|
||||
.withTags(false)
|
||||
.build();
|
||||
const dispatch = jest.fn().mockResolvedValue({});
|
||||
const observable = of([{ text: 'A text' }]).pipe(updateTagsState({ variable, dispatch }));
|
||||
|
||||
observableTester().subscribeAndExpectOnNext({
|
||||
observable,
|
||||
expect: value => {
|
||||
expect(value).toEqual(undefined);
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
},
|
||||
done,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('runUpdateTagsRequest', () => {
|
||||
describe('when called with a datasource with metricFindQuery and variable that uses Tags and refreshes on time range changes', () => {
|
||||
it('then the correct observable should be created', done => {
|
||||
const variable = queryBuilder()
|
||||
.withId('query')
|
||||
.withTags(true)
|
||||
.withTagsQuery('A tags query')
|
||||
.withRefresh(VariableRefresh.onTimeRangeChanged)
|
||||
.build();
|
||||
const timeSrv: any = {
|
||||
timeRange: jest.fn(),
|
||||
};
|
||||
const datasource: any = { metricFindQuery: jest.fn().mockResolvedValue([{ text: 'A text' }]) };
|
||||
const searchFilter = 'A search filter';
|
||||
const observable = of(undefined).pipe(runUpdateTagsRequest({ variable, datasource, searchFilter }, timeSrv));
|
||||
|
||||
observableTester().subscribeAndExpectOnNext({
|
||||
observable,
|
||||
expect: value => {
|
||||
const { index, global, ...rest } = initialQueryVariableModelState;
|
||||
expect(value).toEqual([{ text: 'A text' }]);
|
||||
expect(timeSrv.timeRange).toHaveBeenCalledTimes(1);
|
||||
expect(datasource.metricFindQuery).toHaveBeenCalledTimes(1);
|
||||
expect(datasource.metricFindQuery).toHaveBeenCalledWith('A tags query', {
|
||||
range: undefined,
|
||||
searchFilter: 'A search filter',
|
||||
variable: {
|
||||
...rest,
|
||||
id: 'query',
|
||||
name: 'query',
|
||||
useTags: true,
|
||||
tagsQuery: 'A tags query',
|
||||
refresh: VariableRefresh.onTimeRangeChanged,
|
||||
},
|
||||
});
|
||||
},
|
||||
done,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a datasource without metricFindQuery and variable that uses Tags and refreshes on time range changes', () => {
|
||||
it('then the correct observable should be created', done => {
|
||||
const variable = queryBuilder()
|
||||
.withId('query')
|
||||
.withTags(true)
|
||||
.withTagsQuery('A tags query')
|
||||
.withRefresh(VariableRefresh.onTimeRangeChanged)
|
||||
.build();
|
||||
const timeSrv: any = {
|
||||
timeRange: jest.fn(),
|
||||
};
|
||||
const datasource: any = {};
|
||||
const searchFilter = 'A search filter';
|
||||
const observable = of(undefined).pipe(runUpdateTagsRequest({ variable, datasource, searchFilter }, timeSrv));
|
||||
|
||||
observableTester().subscribeAndExpectOnNext({
|
||||
observable,
|
||||
expect: value => {
|
||||
expect(value).toEqual([]);
|
||||
expect(timeSrv.timeRange).not.toHaveBeenCalled();
|
||||
},
|
||||
done,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called with a datasource with metricFindQuery and variable that does not use Tags but refreshes on time range changes', () => {
|
||||
it('then the correct observable should be created', done => {
|
||||
const variable = queryBuilder()
|
||||
.withId('query')
|
||||
.withTags(false)
|
||||
.withRefresh(VariableRefresh.onTimeRangeChanged)
|
||||
.build();
|
||||
const timeSrv: any = {
|
||||
timeRange: jest.fn(),
|
||||
};
|
||||
const datasource: any = { metricFindQuery: jest.fn().mockResolvedValue([{ text: 'A text' }]) };
|
||||
const searchFilter = 'A search filter';
|
||||
const observable = of(undefined).pipe(runUpdateTagsRequest({ variable, datasource, searchFilter }, timeSrv));
|
||||
|
||||
observableTester().subscribeAndExpectOnNext({
|
||||
observable,
|
||||
expect: value => {
|
||||
expect(value).toEqual([]);
|
||||
expect(timeSrv.timeRange).not.toHaveBeenCalled();
|
||||
expect(datasource.metricFindQuery).not.toHaveBeenCalled();
|
||||
},
|
||||
done,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOptionsState', () => {
|
||||
describe('when called', () => {
|
||||
it('then the correct observable should be created', done => {
|
||||
const variable = queryBuilder()
|
||||
.withId('query')
|
||||
.build();
|
||||
const dispatch = jest.fn();
|
||||
const getTemplatedRegexFunc = jest.fn().mockReturnValue('getTemplatedRegexFunc result');
|
||||
|
||||
const observable = of([{ text: 'A' }]).pipe(updateOptionsState({ variable, dispatch, getTemplatedRegexFunc }));
|
||||
|
||||
observableTester().subscribeAndExpectOnNext({
|
||||
observable,
|
||||
expect: value => {
|
||||
expect(value).toEqual(undefined);
|
||||
expect(getTemplatedRegexFunc).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
updateVariableOptions({
|
||||
id: 'query',
|
||||
type: 'query',
|
||||
data: { results: [{ text: 'A' }], templatedRegex: 'getTemplatedRegexFunc result' },
|
||||
})
|
||||
);
|
||||
},
|
||||
done,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toMetricFindValues', () => {
|
||||
const frameWithTextField = toDataFrame({
|
||||
fields: [{ name: 'text', type: FieldType.string, values: ['A', 'B', 'C'] }],
|
||||
});
|
||||
const frameWithValueField = toDataFrame({
|
||||
fields: [{ name: 'value', type: FieldType.string, values: ['A', 'B', 'C'] }],
|
||||
});
|
||||
const frameWithTextAndValueField = toDataFrame({
|
||||
fields: [
|
||||
{ name: 'text', type: FieldType.string, values: ['TA', 'TB', 'TC'] },
|
||||
{ name: 'value', type: FieldType.string, values: ['VA', 'VB', 'VC'] },
|
||||
],
|
||||
});
|
||||
const frameWithAStringField = toDataFrame({
|
||||
fields: [{ name: 'label', type: FieldType.string, values: ['A', 'B', 'C'] }],
|
||||
});
|
||||
const frameWithExpandableField = toDataFrame({
|
||||
fields: [
|
||||
{ name: 'label', type: FieldType.string, values: ['A', 'B', 'C'] },
|
||||
{ name: 'expandable', type: FieldType.boolean, values: [true, false, true] },
|
||||
],
|
||||
});
|
||||
|
||||
// it.each wouldn't work here as we need the done callback
|
||||
[
|
||||
{ series: null, expected: [] },
|
||||
{ series: undefined, expected: [] },
|
||||
{ series: [], expected: [] },
|
||||
{ series: [{ text: '' }], expected: [{ text: '' }] },
|
||||
{ series: [{ value: '' }], expected: [{ value: '' }] },
|
||||
{
|
||||
series: [frameWithTextField],
|
||||
expected: [
|
||||
{ text: 'A', value: 'A' },
|
||||
{ text: 'B', value: 'B' },
|
||||
{ text: 'C', value: 'C' },
|
||||
],
|
||||
},
|
||||
{
|
||||
series: [frameWithValueField],
|
||||
expected: [
|
||||
{ text: 'A', value: 'A' },
|
||||
{ text: 'B', value: 'B' },
|
||||
{ text: 'C', value: 'C' },
|
||||
],
|
||||
},
|
||||
{
|
||||
series: [frameWithTextAndValueField],
|
||||
expected: [
|
||||
{ text: 'TA', value: 'VA' },
|
||||
{ text: 'TB', value: 'VB' },
|
||||
{ text: 'TC', value: 'VC' },
|
||||
],
|
||||
},
|
||||
{
|
||||
series: [frameWithAStringField],
|
||||
expected: [
|
||||
{ text: 'A', value: 'A' },
|
||||
{ text: 'B', value: 'B' },
|
||||
{ text: 'C', value: 'C' },
|
||||
],
|
||||
},
|
||||
{
|
||||
series: [frameWithExpandableField],
|
||||
expected: [
|
||||
{ text: 'A', value: 'A', expandable: true },
|
||||
{ text: 'B', value: 'B', expandable: false },
|
||||
{ text: 'C', value: 'C', expandable: true },
|
||||
],
|
||||
},
|
||||
].map(scenario => {
|
||||
it(`when called with series:${JSON.stringify(scenario.series, null, 0)}`, done => {
|
||||
const { series, expected } = scenario;
|
||||
const panelData: any = { series };
|
||||
const observable = of(panelData).pipe(toMetricFindValues());
|
||||
|
||||
observableTester().subscribeAndExpectOnNext({
|
||||
observable,
|
||||
expect: value => {
|
||||
expect(value).toEqual(expected);
|
||||
},
|
||||
done,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when called without metric find values and string fields', () => {
|
||||
it('then the observable throws', done => {
|
||||
const frameWithTimeField = toDataFrame({
|
||||
fields: [{ name: 'time', type: FieldType.time, values: [1, 2, 3] }],
|
||||
});
|
||||
|
||||
const panelData: any = { series: [frameWithTimeField] };
|
||||
const observable = of(panelData).pipe(toMetricFindValues());
|
||||
|
||||
observableTester().subscribeAndExpectOnError({
|
||||
observable,
|
||||
expect: value => {
|
||||
expect(value).toEqual(new Error("Couldn't find any field of type string in the results."));
|
||||
},
|
||||
done,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('areMetricFindValues', () => {
|
||||
it.each`
|
||||
values | expected
|
||||
${null} | ${false}
|
||||
${undefined} | ${false}
|
||||
${[]} | ${true}
|
||||
${[{ text: '' }]} | ${true}
|
||||
${[{ Text: '' }]} | ${true}
|
||||
${[{ value: '' }]} | ${true}
|
||||
${[{ Value: '' }]} | ${true}
|
||||
${[{ text: '', value: '' }]} | ${true}
|
||||
${[{ Text: '', Value: '' }]} | ${true}
|
||||
`('when called with values:$values', ({ values, expected }) => {
|
||||
expect(areMetricFindValues(values)).toBe(expected);
|
||||
});
|
||||
});
|
||||
187
public/app/features/variables/query/operators.ts
Normal file
187
public/app/features/variables/query/operators.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { from, of, OperatorFunction } from 'rxjs';
|
||||
import { map, mergeMap } from 'rxjs/operators';
|
||||
|
||||
import { QueryVariableModel } from '../types';
|
||||
import { ThunkDispatch } from '../../../types';
|
||||
import { toVariableIdentifier, toVariablePayload } from '../state/types';
|
||||
import { validateVariableSelectionState } from '../state/actions';
|
||||
import { DataSourceApi, FieldType, getFieldDisplayName, MetricFindValue, PanelData } from '@grafana/data';
|
||||
import { updateVariableOptions, updateVariableTags } from './reducer';
|
||||
import { getTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
import { getLegacyQueryOptions, getTemplatedRegex } from '../utils';
|
||||
|
||||
const metricFindValueProps = ['text', 'Text', 'value', 'Value'];
|
||||
|
||||
export function toMetricFindValues(): OperatorFunction<PanelData, MetricFindValue[]> {
|
||||
return source =>
|
||||
source.pipe(
|
||||
map(panelData => {
|
||||
const frames = panelData.series;
|
||||
if (!frames || !frames.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (areMetricFindValues(frames)) {
|
||||
return frames;
|
||||
}
|
||||
|
||||
const metrics: MetricFindValue[] = [];
|
||||
|
||||
let valueIndex = -1;
|
||||
let textIndex = -1;
|
||||
let stringIndex = -1;
|
||||
let expandableIndex = -1;
|
||||
|
||||
for (const frame of frames) {
|
||||
for (let index = 0; index < frame.fields.length; index++) {
|
||||
const field = frame.fields[index];
|
||||
const fieldName = getFieldDisplayName(field, frame, frames).toLowerCase();
|
||||
|
||||
if (field.type === FieldType.string && stringIndex === -1) {
|
||||
stringIndex = index;
|
||||
}
|
||||
|
||||
if (fieldName === 'text' && field.type === FieldType.string && textIndex === -1) {
|
||||
textIndex = index;
|
||||
}
|
||||
|
||||
if (fieldName === 'value' && field.type === FieldType.string && valueIndex === -1) {
|
||||
valueIndex = index;
|
||||
}
|
||||
|
||||
if (
|
||||
fieldName === 'expandable' &&
|
||||
(field.type === FieldType.boolean || field.type === FieldType.number) &&
|
||||
expandableIndex === -1
|
||||
) {
|
||||
expandableIndex = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stringIndex === -1) {
|
||||
throw new Error("Couldn't find any field of type string in the results.");
|
||||
}
|
||||
|
||||
for (const frame of frames) {
|
||||
for (let index = 0; index < frame.length; index++) {
|
||||
const expandable = expandableIndex !== -1 ? frame.fields[expandableIndex].values.get(index) : undefined;
|
||||
const string = frame.fields[stringIndex].values.get(index);
|
||||
const text = textIndex !== -1 ? frame.fields[textIndex].values.get(index) : null;
|
||||
const value = valueIndex !== -1 ? frame.fields[valueIndex].values.get(index) : null;
|
||||
|
||||
if (valueIndex === -1 && textIndex === -1) {
|
||||
metrics.push({ text: string, value: string, expandable });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (valueIndex === -1 && textIndex !== -1) {
|
||||
metrics.push({ text, value: text, expandable });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (valueIndex !== -1 && textIndex === -1) {
|
||||
metrics.push({ text: value, value, expandable });
|
||||
continue;
|
||||
}
|
||||
|
||||
metrics.push({ text, value, expandable });
|
||||
}
|
||||
}
|
||||
|
||||
return metrics;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function updateOptionsState(args: {
|
||||
variable: QueryVariableModel;
|
||||
dispatch: ThunkDispatch;
|
||||
getTemplatedRegexFunc: typeof getTemplatedRegex;
|
||||
}): OperatorFunction<MetricFindValue[], void> {
|
||||
return source =>
|
||||
source.pipe(
|
||||
map(results => {
|
||||
const { variable, dispatch, getTemplatedRegexFunc } = args;
|
||||
const templatedRegex = getTemplatedRegexFunc(variable);
|
||||
const payload = toVariablePayload(variable, { results, templatedRegex });
|
||||
dispatch(updateVariableOptions(payload));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function runUpdateTagsRequest(
|
||||
args: {
|
||||
variable: QueryVariableModel;
|
||||
datasource: DataSourceApi;
|
||||
searchFilter?: string;
|
||||
},
|
||||
timeSrv: TimeSrv = getTimeSrv()
|
||||
): OperatorFunction<void, MetricFindValue[]> {
|
||||
return source =>
|
||||
source.pipe(
|
||||
mergeMap(() => {
|
||||
const { datasource, searchFilter, variable } = args;
|
||||
|
||||
if (variable.useTags && datasource.metricFindQuery) {
|
||||
return from(
|
||||
datasource.metricFindQuery(variable.tagsQuery, getLegacyQueryOptions(variable, searchFilter, timeSrv))
|
||||
);
|
||||
}
|
||||
|
||||
return of([]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function updateTagsState(args: {
|
||||
variable: QueryVariableModel;
|
||||
dispatch: ThunkDispatch;
|
||||
}): OperatorFunction<MetricFindValue[], void> {
|
||||
return source =>
|
||||
source.pipe(
|
||||
map(tagResults => {
|
||||
const { dispatch, variable } = args;
|
||||
|
||||
if (variable.useTags) {
|
||||
dispatch(updateVariableTags(toVariablePayload(variable, tagResults)));
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function validateVariableSelection(args: {
|
||||
variable: QueryVariableModel;
|
||||
dispatch: ThunkDispatch;
|
||||
searchFilter?: string;
|
||||
}): OperatorFunction<void, void> {
|
||||
return source =>
|
||||
source.pipe(
|
||||
mergeMap(() => {
|
||||
const { dispatch, variable, searchFilter } = args;
|
||||
|
||||
// If we are searching options there is no need to validate selection state
|
||||
// This condition was added to as validateVariableSelectionState will update the current value of the variable
|
||||
// So after search and selection the current value is already update so no setValue, refresh & url update is performed
|
||||
// The if statement below fixes https://github.com/grafana/grafana/issues/25671
|
||||
if (!searchFilter) {
|
||||
return from(dispatch(validateVariableSelectionState(toVariableIdentifier(variable))));
|
||||
}
|
||||
|
||||
return of<void>();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function areMetricFindValues(data: any[]): data is MetricFindValue[] {
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!data.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const firstValue: any = data[0];
|
||||
return metricFindValueProps.some(prop => firstValue.hasOwnProperty(prop) && typeof firstValue[prop] === 'string');
|
||||
}
|
||||
316
public/app/features/variables/query/queryRunners.test.ts
Normal file
316
public/app/features/variables/query/queryRunners.test.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import { QueryRunners } from './queryRunners';
|
||||
import { DefaultTimeRange, observableTester, VariableSupportType } from '@grafana/data';
|
||||
import { VariableRefresh } from '../types';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
describe('QueryRunners', () => {
|
||||
describe('when using a legacy data source', () => {
|
||||
const getLegacyTestContext = (variable?: any) => {
|
||||
variable = variable ?? { query: 'A query' };
|
||||
const timeSrv = {
|
||||
timeRange: jest.fn().mockReturnValue(DefaultTimeRange),
|
||||
};
|
||||
const datasource: any = { metricFindQuery: jest.fn().mockResolvedValue([{ text: 'A', value: 'A' }]) };
|
||||
const runner = new QueryRunners().getRunnerForDatasource(datasource);
|
||||
const runRequest = jest.fn().mockReturnValue(of({}));
|
||||
const runnerArgs: any = { datasource, variable, searchFilter: 'A searchFilter', timeSrv, runRequest };
|
||||
const request: any = {};
|
||||
|
||||
return { timeSrv, datasource, runner, variable, runnerArgs, request };
|
||||
};
|
||||
|
||||
describe('and calling getRunnerForDatasource', () => {
|
||||
it('then it should return LegacyQueryRunner', () => {
|
||||
const { runner } = getLegacyTestContext();
|
||||
expect(runner!.type).toEqual(VariableSupportType.Legacy);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and calling getTarget', () => {
|
||||
it('then it should return correct target', () => {
|
||||
const { runner, datasource, variable } = getLegacyTestContext();
|
||||
const target = runner.getTarget({ datasource, variable });
|
||||
expect(target).toEqual('A query');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and calling runRequest with a variable that refreshes when time range changes', () => {
|
||||
const { datasource, runner, runnerArgs, request, timeSrv } = getLegacyTestContext({
|
||||
query: 'A query',
|
||||
refresh: VariableRefresh.onTimeRangeChanged,
|
||||
});
|
||||
const observable = runner.runRequest(runnerArgs, request);
|
||||
|
||||
it('then it should return correct observable', done => {
|
||||
observableTester().subscribeAndExpectOnNext({
|
||||
observable,
|
||||
expect: values => {
|
||||
expect(values).toEqual({
|
||||
series: [{ text: 'A', value: 'A' }],
|
||||
state: 'Done',
|
||||
timeRange: { from: {}, raw: { from: '6h', to: 'now' }, to: {} },
|
||||
});
|
||||
},
|
||||
done,
|
||||
});
|
||||
});
|
||||
|
||||
it('and it should call timeSrv.timeRange()', () => {
|
||||
expect(timeSrv.timeRange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('and it should call metricFindQuery with correct options', () => {
|
||||
expect(datasource.metricFindQuery).toHaveBeenCalledTimes(1);
|
||||
expect(datasource.metricFindQuery).toHaveBeenCalledWith('A query', {
|
||||
range: {
|
||||
from: {},
|
||||
raw: {
|
||||
from: '6h',
|
||||
to: 'now',
|
||||
},
|
||||
to: {},
|
||||
},
|
||||
searchFilter: 'A searchFilter',
|
||||
variable: {
|
||||
query: 'A query',
|
||||
refresh: VariableRefresh.onTimeRangeChanged,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and calling runRequest with a variable that does not refresh when time range changes', () => {
|
||||
const { datasource, runner, runnerArgs, request, timeSrv } = getLegacyTestContext({
|
||||
query: 'A query',
|
||||
refresh: VariableRefresh.never,
|
||||
});
|
||||
const observable = runner.runRequest(runnerArgs, request);
|
||||
|
||||
it('then it should return correct observable', done => {
|
||||
observableTester().subscribeAndExpectOnNext({
|
||||
observable,
|
||||
expect: values => {
|
||||
expect(values).toEqual({
|
||||
series: [{ text: 'A', value: 'A' }],
|
||||
state: 'Done',
|
||||
timeRange: { from: {}, raw: { from: '6h', to: 'now' }, to: {} },
|
||||
});
|
||||
},
|
||||
done,
|
||||
});
|
||||
});
|
||||
|
||||
it('and it should not call timeSrv.timeRange()', () => {
|
||||
expect(timeSrv.timeRange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('and it should call metricFindQuery with correct options', () => {
|
||||
expect(datasource.metricFindQuery).toHaveBeenCalledTimes(1);
|
||||
expect(datasource.metricFindQuery).toHaveBeenCalledWith('A query', {
|
||||
range: undefined,
|
||||
searchFilter: 'A searchFilter',
|
||||
variable: {
|
||||
query: 'A query',
|
||||
refresh: VariableRefresh.never,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using a data source with standard variable support', () => {
|
||||
const getStandardTestContext = (datasource?: any) => {
|
||||
const variable: any = { query: { refId: 'A', query: 'A query' } };
|
||||
const timeSrv = {};
|
||||
datasource = datasource ?? {
|
||||
variables: {
|
||||
getType: () => VariableSupportType.Standard,
|
||||
toDataQuery: (query: any) => ({ ...query, extra: 'extra' }),
|
||||
},
|
||||
};
|
||||
const runner = new QueryRunners().getRunnerForDatasource(datasource);
|
||||
const runRequest = jest.fn().mockReturnValue(of({}));
|
||||
const runnerArgs: any = { datasource, variable, searchFilter: 'A searchFilter', timeSrv, runRequest };
|
||||
const request: any = {};
|
||||
|
||||
return { timeSrv, datasource, runner, variable, runnerArgs, request, runRequest };
|
||||
};
|
||||
|
||||
describe('and calling getRunnerForDatasource', () => {
|
||||
it('then it should return StandardQueryRunner', () => {
|
||||
const { runner } = getStandardTestContext();
|
||||
expect(runner!.type).toEqual(VariableSupportType.Standard);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and calling getTarget', () => {
|
||||
it('then it should return correct target', () => {
|
||||
const { runner, variable, datasource } = getStandardTestContext();
|
||||
const target = runner.getTarget({ datasource, variable });
|
||||
expect(target).toEqual({ refId: 'A', query: 'A query', extra: 'extra' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('and calling runRequest with a datasource that uses a custom query', () => {
|
||||
const { runner, request, runnerArgs, runRequest, datasource } = getStandardTestContext({
|
||||
variables: {
|
||||
getType: () => VariableSupportType.Standard,
|
||||
toDataQuery: () => undefined,
|
||||
query: () => undefined,
|
||||
},
|
||||
});
|
||||
const observable = runner.runRequest(runnerArgs, request);
|
||||
|
||||
it('then it should return correct observable', done => {
|
||||
observableTester().subscribeAndExpectOnNext({
|
||||
observable,
|
||||
expect: value => {
|
||||
expect(value).toEqual({});
|
||||
},
|
||||
done,
|
||||
});
|
||||
});
|
||||
|
||||
it('then it should call runRequest with correct args', () => {
|
||||
expect(runRequest).toHaveBeenCalledTimes(1);
|
||||
expect(runRequest).toHaveBeenCalledWith(datasource, {}, datasource.variables.query);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and calling runRequest with a datasource that has no custom query', () => {
|
||||
const { runner, request, runnerArgs, runRequest, datasource } = getStandardTestContext({
|
||||
variables: { getType: () => VariableSupportType.Standard, toDataQuery: () => undefined },
|
||||
});
|
||||
const observable = runner.runRequest(runnerArgs, request);
|
||||
|
||||
it('then it should return correct observable', done => {
|
||||
observableTester().subscribeAndExpectOnNext({
|
||||
observable,
|
||||
expect: value => {
|
||||
expect(value).toEqual({});
|
||||
},
|
||||
done,
|
||||
});
|
||||
});
|
||||
|
||||
it('then it should call runRequest with correct args', () => {
|
||||
expect(runRequest).toHaveBeenCalledTimes(1);
|
||||
expect(runRequest).toHaveBeenCalledWith(datasource, {});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using a data source with custom variable support', () => {
|
||||
const getCustomTestContext = () => {
|
||||
const variable: any = { query: { refId: 'A', query: 'A query' } };
|
||||
const timeSrv = {};
|
||||
const datasource: any = {
|
||||
variables: { getType: () => VariableSupportType.Custom, query: () => undefined, editor: {} },
|
||||
};
|
||||
const runner = new QueryRunners().getRunnerForDatasource(datasource);
|
||||
const runRequest = jest.fn().mockReturnValue(of({}));
|
||||
const runnerArgs: any = { datasource, variable, searchFilter: 'A searchFilter', timeSrv, runRequest };
|
||||
const request: any = {};
|
||||
|
||||
return { timeSrv, datasource, runner, variable, runnerArgs, request, runRequest };
|
||||
};
|
||||
|
||||
describe('and calling getRunnerForDatasource', () => {
|
||||
it('then it should return CustomQueryRunner', () => {
|
||||
const { runner } = getCustomTestContext();
|
||||
expect(runner!.type).toEqual(VariableSupportType.Custom);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and calling getTarget', () => {
|
||||
it('then it should return correct target', () => {
|
||||
const { runner, variable, datasource } = getCustomTestContext();
|
||||
const target = runner.getTarget({ datasource, variable });
|
||||
expect(target).toEqual({ refId: 'A', query: 'A query' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('and calling runRequest', () => {
|
||||
const { runner, request, runnerArgs, runRequest, datasource } = getCustomTestContext();
|
||||
const observable = runner.runRequest(runnerArgs, request);
|
||||
|
||||
it('then it should return correct observable', done => {
|
||||
observableTester().subscribeAndExpectOnNext({
|
||||
observable,
|
||||
expect: value => {
|
||||
expect(value).toEqual({});
|
||||
},
|
||||
done,
|
||||
});
|
||||
});
|
||||
|
||||
it('then it should call runRequest with correct args', () => {
|
||||
expect(runRequest).toHaveBeenCalledTimes(1);
|
||||
expect(runRequest).toHaveBeenCalledWith(datasource, {}, datasource.variables.query);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using a data source with datasource variable support', () => {
|
||||
const getDatasourceTestContext = () => {
|
||||
const variable: any = { query: { refId: 'A', query: 'A query' } };
|
||||
const timeSrv = {};
|
||||
const datasource: any = {
|
||||
variables: { getType: () => VariableSupportType.Datasource },
|
||||
};
|
||||
const runner = new QueryRunners().getRunnerForDatasource(datasource);
|
||||
const runRequest = jest.fn().mockReturnValue(of({}));
|
||||
const runnerArgs: any = { datasource, variable, searchFilter: 'A searchFilter', timeSrv, runRequest };
|
||||
const request: any = {};
|
||||
|
||||
return { timeSrv, datasource, runner, variable, runnerArgs, request, runRequest };
|
||||
};
|
||||
|
||||
describe('and calling getRunnerForDatasource', () => {
|
||||
it('then it should return DatasourceQueryRunner', () => {
|
||||
const { runner } = getDatasourceTestContext();
|
||||
expect(runner!.type).toEqual(VariableSupportType.Datasource);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and calling getTarget', () => {
|
||||
it('then it should return correct target', () => {
|
||||
const { runner, datasource, variable } = getDatasourceTestContext();
|
||||
const target = runner.getTarget({ datasource, variable });
|
||||
expect(target).toEqual({ refId: 'A', query: 'A query' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('and calling runRequest', () => {
|
||||
const { runner, request, runnerArgs, runRequest, datasource } = getDatasourceTestContext();
|
||||
const observable = runner.runRequest(runnerArgs, request);
|
||||
|
||||
it('then it should return correct observable', done => {
|
||||
observableTester().subscribeAndExpectOnNext({
|
||||
observable,
|
||||
expect: value => {
|
||||
expect(value).toEqual({});
|
||||
},
|
||||
done,
|
||||
});
|
||||
});
|
||||
|
||||
it('then it should call runRequest with correct args', () => {
|
||||
expect(runRequest).toHaveBeenCalledTimes(1);
|
||||
expect(runRequest).toHaveBeenCalledWith(datasource, {});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using a data source with unknown variable support', () => {
|
||||
describe('and calling getRunnerForDatasource', () => {
|
||||
it('then it should throw', () => {
|
||||
const datasource: any = {
|
||||
variables: {},
|
||||
};
|
||||
|
||||
expect(() => new QueryRunners().getRunnerForDatasource(datasource)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
178
public/app/features/variables/query/queryRunners.ts
Normal file
178
public/app/features/variables/query/queryRunners.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { from, Observable, of } from 'rxjs';
|
||||
import { mergeMap } from 'rxjs/operators';
|
||||
import {
|
||||
DataQuery,
|
||||
DataQueryRequest,
|
||||
DataSourceApi,
|
||||
DefaultTimeRange,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
VariableSupportType,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { QueryVariableModel } from '../types';
|
||||
import {
|
||||
hasCustomVariableSupport,
|
||||
hasDatasourceVariableSupport,
|
||||
hasLegacyVariableSupport,
|
||||
hasStandardVariableSupport,
|
||||
} from '../guard';
|
||||
import { getLegacyQueryOptions } from '../utils';
|
||||
import { TimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
|
||||
export interface RunnerArgs {
|
||||
variable: QueryVariableModel;
|
||||
datasource: DataSourceApi;
|
||||
timeSrv: TimeSrv;
|
||||
runRequest: (
|
||||
datasource: DataSourceApi,
|
||||
request: DataQueryRequest,
|
||||
queryFunction?: typeof datasource.query
|
||||
) => Observable<PanelData>;
|
||||
searchFilter?: string;
|
||||
}
|
||||
|
||||
type GetTargetArgs = { datasource: DataSourceApi; variable: QueryVariableModel };
|
||||
|
||||
export interface QueryRunner {
|
||||
type: VariableSupportType;
|
||||
canRun: (dataSource: DataSourceApi) => boolean;
|
||||
getTarget: (args: GetTargetArgs) => DataQuery;
|
||||
runRequest: (args: RunnerArgs, request: DataQueryRequest) => Observable<PanelData>;
|
||||
}
|
||||
|
||||
export class QueryRunners {
|
||||
private readonly runners: QueryRunner[];
|
||||
constructor() {
|
||||
this.runners = [
|
||||
new LegacyQueryRunner(),
|
||||
new StandardQueryRunner(),
|
||||
new CustomQueryRunner(),
|
||||
new DatasourceQueryRunner(),
|
||||
];
|
||||
}
|
||||
|
||||
getRunnerForDatasource(datasource: DataSourceApi): QueryRunner {
|
||||
const runner = this.runners.find(runner => runner.canRun(datasource));
|
||||
if (runner) {
|
||||
return runner;
|
||||
}
|
||||
|
||||
throw new Error("Couldn't find a query runner that matches supplied arguments.");
|
||||
}
|
||||
}
|
||||
|
||||
class LegacyQueryRunner implements QueryRunner {
|
||||
type = VariableSupportType.Legacy;
|
||||
|
||||
canRun(dataSource: DataSourceApi) {
|
||||
return hasLegacyVariableSupport(dataSource);
|
||||
}
|
||||
|
||||
getTarget({ datasource, variable }: GetTargetArgs) {
|
||||
if (hasLegacyVariableSupport(datasource)) {
|
||||
return variable.query;
|
||||
}
|
||||
|
||||
throw new Error("Couldn't create a target with supplied arguments.");
|
||||
}
|
||||
|
||||
runRequest({ datasource, variable, searchFilter, timeSrv }: RunnerArgs, request: DataQueryRequest) {
|
||||
if (!hasLegacyVariableSupport(datasource)) {
|
||||
return getEmptyMetricFindValueObservable();
|
||||
}
|
||||
|
||||
const queryOptions: any = getLegacyQueryOptions(variable, searchFilter, timeSrv);
|
||||
|
||||
return from(datasource.metricFindQuery(variable.query, queryOptions)).pipe(
|
||||
mergeMap(values => {
|
||||
if (!values || !values.length) {
|
||||
return getEmptyMetricFindValueObservable();
|
||||
}
|
||||
|
||||
const series: any = values;
|
||||
return of({ series, state: LoadingState.Done, timeRange: DefaultTimeRange });
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StandardQueryRunner implements QueryRunner {
|
||||
type = VariableSupportType.Standard;
|
||||
|
||||
canRun(dataSource: DataSourceApi) {
|
||||
return hasStandardVariableSupport(dataSource);
|
||||
}
|
||||
|
||||
getTarget({ datasource, variable }: GetTargetArgs) {
|
||||
if (hasStandardVariableSupport(datasource)) {
|
||||
return datasource.variables.toDataQuery(variable.query);
|
||||
}
|
||||
|
||||
throw new Error("Couldn't create a target with supplied arguments.");
|
||||
}
|
||||
|
||||
runRequest({ datasource, runRequest }: RunnerArgs, request: DataQueryRequest) {
|
||||
if (!hasStandardVariableSupport(datasource)) {
|
||||
return getEmptyMetricFindValueObservable();
|
||||
}
|
||||
|
||||
if (!datasource.variables.query) {
|
||||
return runRequest(datasource, request);
|
||||
}
|
||||
|
||||
return runRequest(datasource, request, datasource.variables.query);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomQueryRunner implements QueryRunner {
|
||||
type = VariableSupportType.Custom;
|
||||
|
||||
canRun(dataSource: DataSourceApi) {
|
||||
return hasCustomVariableSupport(dataSource);
|
||||
}
|
||||
|
||||
getTarget({ datasource, variable }: GetTargetArgs) {
|
||||
if (hasCustomVariableSupport(datasource)) {
|
||||
return variable.query;
|
||||
}
|
||||
|
||||
throw new Error("Couldn't create a target with supplied arguments.");
|
||||
}
|
||||
|
||||
runRequest({ datasource, runRequest }: RunnerArgs, request: DataQueryRequest) {
|
||||
if (!hasCustomVariableSupport(datasource)) {
|
||||
return getEmptyMetricFindValueObservable();
|
||||
}
|
||||
|
||||
return runRequest(datasource, request, datasource.variables.query);
|
||||
}
|
||||
}
|
||||
|
||||
class DatasourceQueryRunner implements QueryRunner {
|
||||
type = VariableSupportType.Datasource;
|
||||
|
||||
canRun(dataSource: DataSourceApi) {
|
||||
return hasDatasourceVariableSupport(dataSource);
|
||||
}
|
||||
|
||||
getTarget({ datasource, variable }: GetTargetArgs) {
|
||||
if (hasDatasourceVariableSupport(datasource)) {
|
||||
return variable.query;
|
||||
}
|
||||
|
||||
throw new Error("Couldn't create a target with supplied arguments.");
|
||||
}
|
||||
|
||||
runRequest({ datasource, runRequest }: RunnerArgs, request: DataQueryRequest) {
|
||||
if (!hasDatasourceVariableSupport(datasource)) {
|
||||
return getEmptyMetricFindValueObservable();
|
||||
}
|
||||
|
||||
return runRequest(datasource, request);
|
||||
}
|
||||
}
|
||||
|
||||
function getEmptyMetricFindValueObservable(): Observable<PanelData> {
|
||||
return of({ state: LoadingState.Done, series: [], timeRange: DefaultTimeRange });
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
initialVariableModelState,
|
||||
QueryVariableModel,
|
||||
VariableOption,
|
||||
VariableQueryEditorType,
|
||||
VariableRefresh,
|
||||
VariableSort,
|
||||
VariableTag,
|
||||
@@ -19,8 +20,6 @@ import {
|
||||
NONE_VARIABLE_VALUE,
|
||||
VariablePayload,
|
||||
} from '../state/types';
|
||||
import { ComponentType } from 'react';
|
||||
import { VariableQueryProps } from '../../../types';
|
||||
import { initialVariablesState, VariablesState } from '../state/variablesReducer';
|
||||
|
||||
interface VariableOptionsUpdate {
|
||||
@@ -29,7 +28,7 @@ interface VariableOptionsUpdate {
|
||||
}
|
||||
|
||||
export interface QueryVariableEditorState {
|
||||
VariableQueryEditor: ComponentType<VariableQueryProps> | null;
|
||||
VariableQueryEditor: VariableQueryEditorType;
|
||||
dataSources: DataSourceSelectItem[];
|
||||
dataSource: DataSourceApi | null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { variableQueryObserver } from './variableQueryObserver';
|
||||
import { LoadingState } from '@grafana/data';
|
||||
import { VariableIdentifier } from '../state/types';
|
||||
import { UpdateOptionsResults } from './VariableQueryRunner';
|
||||
|
||||
function getTestContext(args: { next?: UpdateOptionsResults; error?: any; complete?: boolean }) {
|
||||
const { next, error, complete } = args;
|
||||
const resolve = jest.fn();
|
||||
const reject = jest.fn();
|
||||
const subscription: any = {
|
||||
unsubscribe: jest.fn(),
|
||||
};
|
||||
const observer = variableQueryObserver(resolve, reject, subscription);
|
||||
|
||||
if (next) {
|
||||
observer.next(next);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
observer.error(error);
|
||||
}
|
||||
|
||||
if (complete) {
|
||||
observer.complete();
|
||||
}
|
||||
|
||||
return { resolve, reject, subscription, observer };
|
||||
}
|
||||
|
||||
const identifier: VariableIdentifier = { id: 'id', type: 'query' };
|
||||
|
||||
describe('variableQueryObserver', () => {
|
||||
describe('when receiving a Done state', () => {
|
||||
it('then it should call unsubscribe', () => {
|
||||
const { subscription } = getTestContext({ next: { state: LoadingState.Done, identifier } });
|
||||
|
||||
expect(subscription.unsubscribe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('then it should call resolve', () => {
|
||||
const { resolve } = getTestContext({ next: { state: LoadingState.Done, identifier } });
|
||||
|
||||
expect(resolve).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when receiving an Error state', () => {
|
||||
it('then it should call unsubscribe', () => {
|
||||
const { subscription } = getTestContext({ next: { state: LoadingState.Error, identifier, error: 'An error' } });
|
||||
|
||||
expect(subscription.unsubscribe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('then it should call reject', () => {
|
||||
const { reject } = getTestContext({ next: { state: LoadingState.Error, identifier, error: 'An error' } });
|
||||
|
||||
expect(reject).toHaveBeenCalledTimes(1);
|
||||
expect(reject).toHaveBeenCalledWith('An error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when receiving an error', () => {
|
||||
it('then it should call unsubscribe', () => {
|
||||
const { subscription } = getTestContext({ error: 'An error' });
|
||||
|
||||
expect(subscription.unsubscribe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('then it should call reject', () => {
|
||||
const { reject } = getTestContext({ error: 'An error' });
|
||||
|
||||
expect(reject).toHaveBeenCalledTimes(1);
|
||||
expect(reject).toHaveBeenCalledWith('An error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when receiving complete', () => {
|
||||
it('then it should call unsubscribe', () => {
|
||||
const { subscription } = getTestContext({ complete: true });
|
||||
|
||||
expect(subscription.unsubscribe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('then it should call resolve', () => {
|
||||
const { resolve } = getTestContext({ complete: true });
|
||||
|
||||
expect(resolve).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
36
public/app/features/variables/query/variableQueryObserver.ts
Normal file
36
public/app/features/variables/query/variableQueryObserver.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Observer, Subscription } from 'rxjs';
|
||||
import { LoadingState } from '@grafana/data';
|
||||
|
||||
import { UpdateOptionsResults } from './VariableQueryRunner';
|
||||
|
||||
export function variableQueryObserver(
|
||||
resolve: (value?: any) => void,
|
||||
reject: (value?: any) => void,
|
||||
subscription: Subscription
|
||||
): Observer<UpdateOptionsResults> {
|
||||
const observer: Observer<UpdateOptionsResults> = {
|
||||
next: results => {
|
||||
if (results.state === LoadingState.Error) {
|
||||
subscription.unsubscribe();
|
||||
reject(results.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.state === LoadingState.Done) {
|
||||
subscription.unsubscribe();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
},
|
||||
error: err => {
|
||||
subscription.unsubscribe();
|
||||
reject(err);
|
||||
},
|
||||
complete: () => {
|
||||
subscription.unsubscribe();
|
||||
resolve();
|
||||
},
|
||||
};
|
||||
|
||||
return observer;
|
||||
}
|
||||
@@ -10,11 +10,12 @@ import { initialTextBoxVariableModelState } from '../../textbox/reducer';
|
||||
import { initialCustomVariableModelState } from '../../custom/reducer';
|
||||
import { MultiVariableBuilder } from './multiVariableBuilder';
|
||||
import { initialConstantVariableModelState } from '../../constant/reducer';
|
||||
import { QueryVariableBuilder } from './queryVariableBuilder';
|
||||
|
||||
export const adHocBuilder = () => new AdHocVariableBuilder(initialAdHocVariableModelState);
|
||||
export const intervalBuilder = () => new IntervalVariableBuilder(initialIntervalVariableModelState);
|
||||
export const datasourceBuilder = () => new DatasourceVariableBuilder(initialDataSourceVariableModelState);
|
||||
export const queryBuilder = () => new DatasourceVariableBuilder(initialQueryVariableModelState);
|
||||
export const queryBuilder = () => new QueryVariableBuilder(initialQueryVariableModelState);
|
||||
export const textboxBuilder = () => new OptionsVariableBuilder(initialTextBoxVariableModelState);
|
||||
export const customBuilder = () => new MultiVariableBuilder(initialCustomVariableModelState);
|
||||
export const constantBuilder = () => new OptionsVariableBuilder(initialConstantVariableModelState);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { QueryVariableModel } from 'app/features/variables/types';
|
||||
import { DatasourceVariableBuilder } from './datasourceVariableBuilder';
|
||||
|
||||
export class QueryVariableBuilder<T extends QueryVariableModel> extends DatasourceVariableBuilder<T> {
|
||||
withTags(useTags: boolean) {
|
||||
this.variable.useTags = useTags;
|
||||
return this;
|
||||
}
|
||||
|
||||
withTagsQuery(tagsQuery: string) {
|
||||
this.variable.tagsQuery = tagsQuery;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ import { cleanVariables } from './variablesReducer';
|
||||
import { expect } from '../../../../test/lib/common';
|
||||
import { VariableRefresh } from '../types';
|
||||
import { updateVariableOptions } from '../query/reducer';
|
||||
import { setVariableQueryRunner, VariableQueryRunner } from '../query/VariableQueryRunner';
|
||||
|
||||
variableAdapters.setInit(() => [
|
||||
createQueryVariableAdapter(),
|
||||
@@ -178,6 +179,7 @@ describe('shared actions', () => {
|
||||
|
||||
// Fix for https://github.com/grafana/grafana/issues/28791
|
||||
it('fix for https://github.com/grafana/grafana/issues/28791', async () => {
|
||||
setVariableQueryRunner(new VariableQueryRunner());
|
||||
const stats = queryBuilder()
|
||||
.withId('stats')
|
||||
.withName('stats')
|
||||
|
||||
@@ -13,6 +13,7 @@ import { VariableRefresh } from '../types';
|
||||
import { updateVariableOptions } from '../query/reducer';
|
||||
import { customBuilder, queryBuilder } from '../shared/testing/builders';
|
||||
import { variablesInitTransaction } from './transactionReducer';
|
||||
import { setVariableQueryRunner, VariableQueryRunner } from '../query/VariableQueryRunner';
|
||||
|
||||
jest.mock('app/features/dashboard/services/TimeSrv', () => ({
|
||||
getTimeSrv: jest.fn().mockReturnValue({
|
||||
@@ -94,6 +95,7 @@ describe('processVariable', () => {
|
||||
.build();
|
||||
|
||||
const list = [custom, queryDependsOnCustom, queryNoDepends];
|
||||
setVariableQueryRunner(new VariableQueryRunner());
|
||||
|
||||
return {
|
||||
custom,
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { LoadingState, VariableModel as BaseVariableModel, VariableType } from '@grafana/data';
|
||||
import { ComponentType } from 'react';
|
||||
import {
|
||||
DataQuery,
|
||||
DataSourceJsonData,
|
||||
LoadingState,
|
||||
QueryEditorProps,
|
||||
VariableModel as BaseVariableModel,
|
||||
VariableType,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { NEW_VARIABLE_ID } from './state/types';
|
||||
import { VariableQueryProps } from '../../types';
|
||||
|
||||
export enum VariableRefresh {
|
||||
never,
|
||||
@@ -73,6 +83,7 @@ export interface QueryVariableModel extends DataSourceVariableModel {
|
||||
tagValuesQuery: string;
|
||||
useTags: boolean;
|
||||
queryValue?: string;
|
||||
query: any;
|
||||
}
|
||||
|
||||
export interface TextBoxVariableModel extends VariableWithOptions {}
|
||||
@@ -142,3 +153,8 @@ export const initialVariableModelState: VariableModel = {
|
||||
state: LoadingState.NotStarted,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export type VariableQueryEditorType<
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
> = ComponentType<VariableQueryProps> | ComponentType<QueryEditorProps<any, TQuery, TOptions, any>> | null;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import isString from 'lodash/isString';
|
||||
import { ScopedVars } from '@grafana/data';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
|
||||
import { ALL_VARIABLE_TEXT } from './state/types';
|
||||
import { QueryVariableModel, VariableModel, VariableRefresh } from './types';
|
||||
import { getTimeSrv } from '../dashboard/services/TimeSrv';
|
||||
|
||||
/*
|
||||
* This regex matches 3 types of variable reference with an optional format specifier
|
||||
@@ -105,6 +108,27 @@ export const getCurrentText = (variable: any): string => {
|
||||
return variable.current.text;
|
||||
};
|
||||
|
||||
export function getTemplatedRegex(variable: QueryVariableModel, templateSrv = getTemplateSrv()): string {
|
||||
if (!variable) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!variable.regex) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return templateSrv.replace(variable.regex, {}, 'regex');
|
||||
}
|
||||
|
||||
export function getLegacyQueryOptions(variable: QueryVariableModel, searchFilter?: string, timeSrv = getTimeSrv()) {
|
||||
const queryOptions: any = { range: undefined, variable, searchFilter };
|
||||
if (variable.refresh === VariableRefresh.onTimeRangeChanged) {
|
||||
queryOptions.range = timeSrv.timeRange();
|
||||
}
|
||||
|
||||
return queryOptions;
|
||||
}
|
||||
|
||||
export function getVariableRefresh(variable: VariableModel): VariableRefresh {
|
||||
if (!variable || !variable.hasOwnProperty('refresh')) {
|
||||
return VariableRefresh.never;
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import isString from 'lodash/isString';
|
||||
import { alignmentPeriods, ValueTypes, MetricKind, selectors } from './constants';
|
||||
import { alignmentPeriods, MetricKind, selectors, ValueTypes } from './constants';
|
||||
import CloudMonitoringDatasource from './datasource';
|
||||
import { MetricFindQueryTypes, VariableQueryData } from './types';
|
||||
import { CloudMonitoringVariableQuery, MetricFindQueryTypes } from './types';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import {
|
||||
getMetricTypesByService,
|
||||
getAlignmentOptionsByMetric,
|
||||
getAggregationOptionsByMetric,
|
||||
extractServicesFromMetricDescriptors,
|
||||
getAggregationOptionsByMetric,
|
||||
getAlignmentOptionsByMetric,
|
||||
getLabelKeys,
|
||||
getMetricTypesByService,
|
||||
} from './functions';
|
||||
|
||||
export default class CloudMonitoringMetricFindQuery {
|
||||
constructor(private datasource: CloudMonitoringDatasource) {}
|
||||
|
||||
async execute(query: VariableQueryData) {
|
||||
async execute(query: CloudMonitoringVariableQuery) {
|
||||
try {
|
||||
if (!query.projectName) {
|
||||
query.projectName = this.datasource.getDefaultProject();
|
||||
@@ -63,7 +63,7 @@ export default class CloudMonitoringMetricFindQuery {
|
||||
}));
|
||||
}
|
||||
|
||||
async handleServiceQuery({ projectName }: VariableQueryData) {
|
||||
async handleServiceQuery({ projectName }: CloudMonitoringVariableQuery) {
|
||||
const metricDescriptors = await this.datasource.getMetricTypes(projectName);
|
||||
const services: any[] = extractServicesFromMetricDescriptors(metricDescriptors);
|
||||
return services.map(s => ({
|
||||
@@ -73,7 +73,7 @@ export default class CloudMonitoringMetricFindQuery {
|
||||
}));
|
||||
}
|
||||
|
||||
async handleMetricTypesQuery({ selectedService, projectName }: VariableQueryData) {
|
||||
async handleMetricTypesQuery({ selectedService, projectName }: CloudMonitoringVariableQuery) {
|
||||
if (!selectedService) {
|
||||
return [];
|
||||
}
|
||||
@@ -87,7 +87,7 @@ export default class CloudMonitoringMetricFindQuery {
|
||||
);
|
||||
}
|
||||
|
||||
async handleLabelKeysQuery({ selectedMetricType, projectName }: VariableQueryData) {
|
||||
async handleLabelKeysQuery({ selectedMetricType, projectName }: CloudMonitoringVariableQuery) {
|
||||
if (!selectedMetricType) {
|
||||
return [];
|
||||
}
|
||||
@@ -95,7 +95,7 @@ export default class CloudMonitoringMetricFindQuery {
|
||||
return labelKeys.map(this.toFindQueryResult);
|
||||
}
|
||||
|
||||
async handleLabelValuesQuery({ selectedMetricType, labelKey, projectName }: VariableQueryData) {
|
||||
async handleLabelValuesQuery({ selectedMetricType, labelKey, projectName }: CloudMonitoringVariableQuery) {
|
||||
if (!selectedMetricType) {
|
||||
return [];
|
||||
}
|
||||
@@ -106,7 +106,7 @@ export default class CloudMonitoringMetricFindQuery {
|
||||
return values.map(this.toFindQueryResult);
|
||||
}
|
||||
|
||||
async handleResourceTypeQuery({ selectedMetricType, projectName }: VariableQueryData) {
|
||||
async handleResourceTypeQuery({ selectedMetricType, projectName }: CloudMonitoringVariableQuery) {
|
||||
if (!selectedMetricType) {
|
||||
return [];
|
||||
}
|
||||
@@ -115,7 +115,7 @@ export default class CloudMonitoringMetricFindQuery {
|
||||
return labels['resource.type'].map(this.toFindQueryResult);
|
||||
}
|
||||
|
||||
async handleAlignersQuery({ selectedMetricType, projectName }: VariableQueryData) {
|
||||
async handleAlignersQuery({ selectedMetricType, projectName }: CloudMonitoringVariableQuery) {
|
||||
if (!selectedMetricType) {
|
||||
return [];
|
||||
}
|
||||
@@ -131,7 +131,7 @@ export default class CloudMonitoringMetricFindQuery {
|
||||
return getAlignmentOptionsByMetric(descriptor.valueType, descriptor.metricKind).map(this.toFindQueryResult);
|
||||
}
|
||||
|
||||
async handleAggregationQuery({ selectedMetricType, projectName }: VariableQueryData) {
|
||||
async handleAggregationQuery({ selectedMetricType, projectName }: CloudMonitoringVariableQuery) {
|
||||
if (!selectedMetricType) {
|
||||
return [];
|
||||
}
|
||||
@@ -150,12 +150,12 @@ export default class CloudMonitoringMetricFindQuery {
|
||||
);
|
||||
}
|
||||
|
||||
async handleSLOServicesQuery({ projectName }: VariableQueryData) {
|
||||
async handleSLOServicesQuery({ projectName }: CloudMonitoringVariableQuery) {
|
||||
const services = await this.datasource.getSLOServices(projectName);
|
||||
return services.map(this.toFindQueryResult);
|
||||
}
|
||||
|
||||
async handleSLOQuery({ selectedSLOService, projectName }: VariableQueryData) {
|
||||
async handleSLOQuery({ selectedSLOService, projectName }: CloudMonitoringVariableQuery) {
|
||||
const slos = await this.datasource.getServiceLevelObjectives(projectName, selectedSLOService);
|
||||
return slos.map(this.toFindQueryResult);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React from 'react';
|
||||
import { LegacyForms } from '@grafana/ui';
|
||||
const { Input } = LegacyForms;
|
||||
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { Metrics, LabelFilter, AnnotationsHelp, Project } from './';
|
||||
import { AnnotationsHelp, LabelFilter, Metrics, Project } from './';
|
||||
import { toOption } from '../functions';
|
||||
import { AnnotationTarget, MetricDescriptor } from '../types';
|
||||
|
||||
const { Input } = LegacyForms;
|
||||
|
||||
export interface Props {
|
||||
onQueryChange: (target: AnnotationTarget) => void;
|
||||
target: AnnotationTarget;
|
||||
@@ -52,7 +52,7 @@ export class AnnotationQueryEditor extends React.Component<Props, State> {
|
||||
|
||||
const variableOptionGroup = {
|
||||
label: 'Template Variables',
|
||||
options: datasource.variables.map(toOption),
|
||||
options: datasource.getVariables().map(toOption),
|
||||
};
|
||||
|
||||
const projects = await datasource.getProjects();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { CoreEvents } from 'app/types';
|
||||
import { MetricQueryEditor, QueryTypeSelector, SLOQueryEditor, Help } from './';
|
||||
import { Help, MetricQueryEditor, QueryTypeSelector, SLOQueryEditor } from './';
|
||||
import { CloudMonitoringQuery, MetricQuery, QueryType, SLOQuery } from '../types';
|
||||
import { defaultQuery } from './MetricQueryEditor';
|
||||
import { defaultQuery as defaultSLOQuery } from './SLOQueryEditor';
|
||||
import { toOption, formatCloudMonitoringError } from '../functions';
|
||||
import { formatCloudMonitoringError, toOption } from '../functions';
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { ExploreQueryFieldProps } from '@grafana/data';
|
||||
|
||||
@@ -71,7 +71,7 @@ export class QueryEditor extends PureComponent<Props, State> {
|
||||
const variableOptionGroup = {
|
||||
label: 'Template Variables',
|
||||
expanded: false,
|
||||
options: datasource.variables.map(toOption),
|
||||
options: datasource.getVariables().map(toOption),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,42 +1,38 @@
|
||||
import React from 'react';
|
||||
// @ts-ignore
|
||||
import renderer from 'react-test-renderer';
|
||||
import { CloudMonitoringVariableQueryEditor } from './VariableQueryEditor';
|
||||
import { VariableQueryProps } from 'app/types/plugins';
|
||||
import { MetricFindQueryTypes } from '../types';
|
||||
import { VariableModel } from 'app/features/variables/types';
|
||||
import { CloudMonitoringVariableQueryEditor, Props } from './VariableQueryEditor';
|
||||
import { CloudMonitoringVariableQuery, MetricFindQueryTypes } from '../types';
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { VariableModel } from '@grafana/data';
|
||||
|
||||
jest.mock('../functions', () => ({
|
||||
getMetricTypes: (): any => ({ metricTypes: [], selectedMetricType: '' }),
|
||||
extractServicesFromMetricDescriptors: (): any[] => [],
|
||||
}));
|
||||
|
||||
jest.mock('../../../../core/config', () => {
|
||||
console.warn('[This test uses old variable system, needs a rewrite]');
|
||||
const original = jest.requireActual('../../../../core/config');
|
||||
const config = original.getConfig();
|
||||
jest.mock('@grafana/runtime', () => {
|
||||
const original = jest.requireActual('@grafana/runtime');
|
||||
return {
|
||||
getConfig: () => ({
|
||||
...config,
|
||||
featureToggles: {
|
||||
...config.featureToggles,
|
||||
newVariables: false,
|
||||
},
|
||||
...original,
|
||||
getTemplateSrv: () => ({
|
||||
replace: (s: string) => s,
|
||||
getVariables: () => ([] as unknown) as VariableModel[],
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const props: VariableQueryProps = {
|
||||
onChange: (query, definition) => {},
|
||||
query: {},
|
||||
datasource: {
|
||||
const props: Props = {
|
||||
onChange: query => {},
|
||||
query: ({} as unknown) as CloudMonitoringVariableQuery,
|
||||
datasource: ({
|
||||
getDefaultProject: () => '',
|
||||
getProjects: async () => Promise.resolve([]),
|
||||
getMetricTypes: async (projectName: string) => Promise.resolve([]),
|
||||
getSLOServices: async (projectName: string, serviceId: string) => Promise.resolve([]),
|
||||
getSLOServices: async (projectName: string) => Promise.resolve([]),
|
||||
getServiceLevelObjectives: (projectName: string, serviceId: string) => Promise.resolve([]),
|
||||
},
|
||||
templateSrv: { replace: (s: string) => s, getVariables: () => ([] as unknown) as VariableModel[] },
|
||||
} as unknown) as CloudMonitoringDatasource,
|
||||
onRunQuery: () => {},
|
||||
};
|
||||
|
||||
describe('VariableQueryEditor', () => {
|
||||
@@ -46,10 +42,9 @@ describe('VariableQueryEditor', () => {
|
||||
});
|
||||
|
||||
describe('and a new variable is created', () => {
|
||||
// these test need to be updated to reflect the changes from old variables system to new
|
||||
it('should trigger a query using the first query type in the array', done => {
|
||||
props.onChange = (query, definition) => {
|
||||
expect(definition).toBe('Google Cloud Monitoring - Projects');
|
||||
props.onChange = query => {
|
||||
expect(query.selectedQueryType).toBe('projects');
|
||||
done();
|
||||
};
|
||||
renderer.create(<CloudMonitoringVariableQueryEditor {...props} />).toJSON();
|
||||
@@ -57,11 +52,10 @@ describe('VariableQueryEditor', () => {
|
||||
});
|
||||
|
||||
describe('and an existing variable is edited', () => {
|
||||
// these test need to be updated to reflect the changes from old variables system to new
|
||||
it('should trigger new query using the saved query type', done => {
|
||||
props.query = { selectedQueryType: MetricFindQueryTypes.LabelKeys };
|
||||
props.onChange = (query, definition) => {
|
||||
expect(definition).toBe('Google Cloud Monitoring - Label Keys');
|
||||
props.query = ({ selectedQueryType: MetricFindQueryTypes.LabelKeys } as unknown) as CloudMonitoringVariableQuery;
|
||||
props.onChange = query => {
|
||||
expect(query.selectedQueryType).toBe('labelKeys');
|
||||
done();
|
||||
};
|
||||
renderer.create(<CloudMonitoringVariableQueryEditor {...props} />).toJSON();
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { VariableQueryProps } from 'app/types/plugins';
|
||||
import { SimpleSelect } from './';
|
||||
import { extractServicesFromMetricDescriptors, getLabelKeys, getMetricTypes } from '../functions';
|
||||
import { MetricFindQueryTypes, VariableQueryData } from '../types';
|
||||
import {
|
||||
CloudMonitoringOptions,
|
||||
CloudMonitoringQuery,
|
||||
CloudMonitoringVariableQuery,
|
||||
MetricDescriptor,
|
||||
MetricFindQueryTypes,
|
||||
VariableQueryData,
|
||||
} from '../types';
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { QueryEditorProps } from '@grafana/data';
|
||||
|
||||
export class CloudMonitoringVariableQueryEditor extends PureComponent<VariableQueryProps, VariableQueryData> {
|
||||
export type Props = QueryEditorProps<
|
||||
CloudMonitoringDatasource,
|
||||
CloudMonitoringQuery,
|
||||
CloudMonitoringOptions,
|
||||
CloudMonitoringVariableQuery
|
||||
>;
|
||||
|
||||
export class CloudMonitoringVariableQueryEditor extends PureComponent<Props, VariableQueryData> {
|
||||
queryTypes: Array<{ value: string; name: string }> = [
|
||||
{ value: MetricFindQueryTypes.Projects, name: 'Projects' },
|
||||
{ value: MetricFindQueryTypes.Services, name: 'Services' },
|
||||
@@ -36,7 +52,7 @@ export class CloudMonitoringVariableQueryEditor extends PureComponent<VariableQu
|
||||
loading: true,
|
||||
};
|
||||
|
||||
constructor(props: VariableQueryProps) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = Object.assign(
|
||||
this.defaults,
|
||||
@@ -46,7 +62,7 @@ export class CloudMonitoringVariableQueryEditor extends PureComponent<VariableQu
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const projects = await this.props.datasource.getProjects();
|
||||
const projects = (await this.props.datasource.getProjects()) as MetricDescriptor[];
|
||||
const metricDescriptors = await this.props.datasource.getMetricTypes(
|
||||
this.props.query.projectName || this.props.datasource.getDefaultProject()
|
||||
);
|
||||
@@ -56,7 +72,7 @@ export class CloudMonitoringVariableQueryEditor extends PureComponent<VariableQu
|
||||
}));
|
||||
|
||||
let selectedService = '';
|
||||
if (services.some(s => s.value === this.props.templateSrv.replace(this.state.selectedService))) {
|
||||
if (services.some(s => s.value === getTemplateSrv().replace(this.state.selectedService))) {
|
||||
selectedService = this.state.selectedService;
|
||||
} else if (services && services.length > 0) {
|
||||
selectedService = services[0].value;
|
||||
@@ -65,8 +81,8 @@ export class CloudMonitoringVariableQueryEditor extends PureComponent<VariableQu
|
||||
const { metricTypes, selectedMetricType } = getMetricTypes(
|
||||
metricDescriptors,
|
||||
this.state.selectedMetricType,
|
||||
this.props.templateSrv.replace(this.state.selectedMetricType),
|
||||
this.props.templateSrv.replace(selectedService)
|
||||
getTemplateSrv().replace(this.state.selectedMetricType),
|
||||
getTemplateSrv().replace(selectedService)
|
||||
);
|
||||
|
||||
const sloServices = await this.props.datasource.getSLOServices(this.state.projectName);
|
||||
@@ -87,8 +103,7 @@ export class CloudMonitoringVariableQueryEditor extends PureComponent<VariableQu
|
||||
|
||||
onPropsChange = () => {
|
||||
const { metricDescriptors, labels, metricTypes, services, ...queryModel } = this.state;
|
||||
const query = this.queryTypes.find(q => q.value === this.state.selectedQueryType)!;
|
||||
this.props.onChange(queryModel, `Google Cloud Monitoring - ${query.name}`);
|
||||
this.props.onChange({ ...queryModel, refId: 'CloudMonitoringVariableQueryEditor-VariableQuery' });
|
||||
};
|
||||
|
||||
async onQueryTypeChange(queryType: string) {
|
||||
@@ -106,8 +121,8 @@ export class CloudMonitoringVariableQueryEditor extends PureComponent<VariableQu
|
||||
const { metricTypes, selectedMetricType } = getMetricTypes(
|
||||
metricDescriptors,
|
||||
this.state.selectedMetricType,
|
||||
this.props.templateSrv.replace(this.state.selectedMetricType),
|
||||
this.props.templateSrv.replace(this.state.selectedService)
|
||||
getTemplateSrv().replace(this.state.selectedMetricType),
|
||||
getTemplateSrv().replace(this.state.selectedService)
|
||||
);
|
||||
|
||||
const sloServices = await this.props.datasource.getSLOServices(projectName);
|
||||
@@ -126,8 +141,8 @@ export class CloudMonitoringVariableQueryEditor extends PureComponent<VariableQu
|
||||
const { metricTypes, selectedMetricType } = getMetricTypes(
|
||||
this.state.metricDescriptors,
|
||||
this.state.selectedMetricType,
|
||||
this.props.templateSrv.replace(this.state.selectedMetricType),
|
||||
this.props.templateSrv.replace(service)
|
||||
getTemplateSrv().replace(this.state.selectedMetricType),
|
||||
getTemplateSrv().replace(service)
|
||||
);
|
||||
const state: any = {
|
||||
selectedService: service,
|
||||
@@ -150,7 +165,7 @@ export class CloudMonitoringVariableQueryEditor extends PureComponent<VariableQu
|
||||
this.setState({ labelKey }, () => this.onPropsChange());
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<VariableQueryProps>, prevState: Readonly<VariableQueryData>) {
|
||||
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<VariableQueryData>) {
|
||||
const selecQueryTypeChanged = prevState.selectedQueryType !== this.state.selectedQueryType;
|
||||
const selectSLOServiceChanged = this.state.selectedSLOService !== prevState.selectedSLOService;
|
||||
if (selecQueryTypeChanged || selectSLOServiceChanged) {
|
||||
@@ -162,7 +177,7 @@ export class CloudMonitoringVariableQueryEditor extends PureComponent<VariableQu
|
||||
let result = { labels: this.state.labels, labelKey: this.state.labelKey };
|
||||
if (selectedMetricType && selectedQueryType === MetricFindQueryTypes.LabelValues) {
|
||||
const labels = await getLabelKeys(this.props.datasource, selectedMetricType, projectName);
|
||||
const labelKey = labels.some(l => l === this.props.templateSrv.replace(this.state.labelKey))
|
||||
const labelKey = labels.some(l => l === getTemplateSrv().replace(this.state.labelKey))
|
||||
? this.state.labelKey
|
||||
: labels[0];
|
||||
result = { labels, labelKey };
|
||||
@@ -171,7 +186,9 @@ export class CloudMonitoringVariableQueryEditor extends PureComponent<VariableQu
|
||||
}
|
||||
|
||||
insertTemplateVariables(options: any) {
|
||||
const templateVariables = this.props.templateSrv.getVariables().map((v: any) => ({
|
||||
const templateVariables = getTemplateSrv()
|
||||
.getVariables()
|
||||
.map((v: any) => ({
|
||||
name: `$${v.name}`,
|
||||
value: `$${v.name}`,
|
||||
}));
|
||||
|
||||
@@ -12,17 +12,10 @@ import {
|
||||
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
|
||||
import {
|
||||
CloudMonitoringQuery,
|
||||
MetricDescriptor,
|
||||
CloudMonitoringOptions,
|
||||
Filter,
|
||||
VariableQueryData,
|
||||
QueryType,
|
||||
} from './types';
|
||||
import { CloudMonitoringOptions, CloudMonitoringQuery, Filter, MetricDescriptor, QueryType } from './types';
|
||||
import { cloudMonitoringUnitMappings } from './constants';
|
||||
import API from './api';
|
||||
import CloudMonitoringMetricFindQuery from './CloudMonitoringMetricFindQuery';
|
||||
import { CloudMonitoringVariableSupport } from './variables';
|
||||
|
||||
export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonitoringQuery, CloudMonitoringOptions> {
|
||||
api: API;
|
||||
@@ -36,9 +29,11 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
|
||||
super(instanceSettings);
|
||||
this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';
|
||||
this.api = new API(`${instanceSettings.url!}/cloudmonitoring/v3/projects/`);
|
||||
|
||||
this.variables = new CloudMonitoringVariableSupport(this);
|
||||
}
|
||||
|
||||
get variables() {
|
||||
getVariables() {
|
||||
return this.templateSrv.getVariables().map(v => `$${v.name}`);
|
||||
}
|
||||
|
||||
@@ -125,12 +120,6 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
|
||||
return results;
|
||||
}
|
||||
|
||||
async metricFindQuery(query: VariableQueryData) {
|
||||
await this.ensureGCEDefaultProject();
|
||||
const cloudMonitoringMetricFindQuery = new CloudMonitoringMetricFindQuery(this);
|
||||
return cloudMonitoringMetricFindQuery.execute(query);
|
||||
}
|
||||
|
||||
async getTimeSeries(options: DataQueryRequest<CloudMonitoringQuery>) {
|
||||
await this.ensureGCEDefaultProject();
|
||||
const queries = options.targets
|
||||
|
||||
@@ -26,6 +26,17 @@ export enum MetricFindQueryTypes {
|
||||
SLO = 'slo',
|
||||
}
|
||||
|
||||
export interface CloudMonitoringVariableQuery extends DataQuery {
|
||||
selectedQueryType: string;
|
||||
selectedService: string;
|
||||
selectedMetricType: string;
|
||||
selectedSLOService: string;
|
||||
labelKey: string;
|
||||
projects: Array<{ value: string; name: string }>;
|
||||
sloServices: Array<{ value: string; name: string }>;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
export interface VariableQueryData {
|
||||
selectedQueryType: string;
|
||||
metricDescriptors: MetricDescriptor[];
|
||||
|
||||
31
public/app/plugins/datasource/cloud-monitoring/variables.ts
Normal file
31
public/app/plugins/datasource/cloud-monitoring/variables.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { from, Observable } from 'rxjs';
|
||||
import { map, mergeMap } from 'rxjs/operators';
|
||||
import { CustomVariableSupport, DataQueryRequest, DataQueryResponse } from '@grafana/data';
|
||||
|
||||
import CloudMonitoringDatasource from './datasource';
|
||||
import { CloudMonitoringVariableQuery } from './types';
|
||||
import CloudMonitoringMetricFindQuery from './CloudMonitoringMetricFindQuery';
|
||||
import { CloudMonitoringVariableQueryEditor } from './components/VariableQueryEditor';
|
||||
|
||||
export class CloudMonitoringVariableSupport extends CustomVariableSupport<
|
||||
CloudMonitoringDatasource,
|
||||
CloudMonitoringVariableQuery
|
||||
> {
|
||||
private readonly metricFindQuery: CloudMonitoringMetricFindQuery;
|
||||
|
||||
constructor(private readonly datasource: CloudMonitoringDatasource) {
|
||||
super();
|
||||
this.metricFindQuery = new CloudMonitoringMetricFindQuery(datasource);
|
||||
this.query = this.query.bind(this);
|
||||
}
|
||||
|
||||
editor = CloudMonitoringVariableQueryEditor;
|
||||
|
||||
query(request: DataQueryRequest<CloudMonitoringVariableQuery>): Observable<DataQueryResponse> {
|
||||
const executeObservable = from(this.metricFindQuery.execute(request.targets[0]));
|
||||
return from(this.datasource.ensureGCEDefaultProject()).pipe(
|
||||
mergeMap(() => executeObservable),
|
||||
map(data => ({ data }))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Segment, SegmentAsync } from '@grafana/ui';
|
||||
import { SelectableStrings, CloudWatchMetricsQuery } from '../types';
|
||||
import { CloudWatchMetricsQuery, SelectableStrings } from '../types';
|
||||
import { CloudWatchDatasource } from '../datasource';
|
||||
import { Stats, Dimensions, QueryInlineField } from '.';
|
||||
import { Dimensions, QueryInlineField, Stats } from '.';
|
||||
|
||||
export type Props = {
|
||||
query: CloudWatchMetricsQuery;
|
||||
@@ -39,7 +39,7 @@ export function MetricsQueryFieldsEditor({
|
||||
useEffect(() => {
|
||||
const variableOptionGroup = {
|
||||
label: 'Template Variables',
|
||||
options: datasource.variables.map(toOption),
|
||||
options: datasource.getVariables().map(toOption),
|
||||
};
|
||||
|
||||
Promise.all([datasource.metricFindQuery('regions()'), datasource.metricFindQuery('namespaces()')]).then(
|
||||
@@ -61,7 +61,7 @@ export function MetricsQueryFieldsEditor({
|
||||
|
||||
const appendTemplateVariables = (values: SelectableValue[]) => [
|
||||
...values,
|
||||
{ label: 'Template Variables', options: datasource.variables.map(toOption) },
|
||||
{ label: 'Template Variables', options: datasource.getVariables().map(toOption) },
|
||||
];
|
||||
|
||||
const toOption = (value: any) => ({ label: value, value });
|
||||
|
||||
@@ -1,28 +1,44 @@
|
||||
import React from 'react';
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { AppNotificationTimeout } from 'app/types';
|
||||
import { store } from 'app/store/store';
|
||||
import { from, merge, Observable, of, zip } from 'rxjs';
|
||||
import {
|
||||
catchError,
|
||||
concatMap,
|
||||
filter,
|
||||
finalize,
|
||||
map,
|
||||
mergeMap,
|
||||
repeat,
|
||||
scan,
|
||||
share,
|
||||
takeWhile,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
import { getBackendSrv, getGrafanaLiveSrv, toDataQueryResponse } from '@grafana/runtime';
|
||||
import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider';
|
||||
import {
|
||||
DataFrame,
|
||||
DataQueryErrorType,
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
dateMath,
|
||||
LoadingState,
|
||||
LogRowModel,
|
||||
ScopedVars,
|
||||
TimeRange,
|
||||
rangeUtil,
|
||||
DataQueryErrorType,
|
||||
LiveChannelScope,
|
||||
LiveChannelEvent,
|
||||
LiveChannelMessageEvent,
|
||||
LiveChannelScope,
|
||||
LoadingState,
|
||||
LogRowModel,
|
||||
rangeUtil,
|
||||
ScopedVars,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { getBackendSrv, getGrafanaLiveSrv, toDataQueryResponse } from '@grafana/runtime';
|
||||
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { AppNotificationTimeout } from 'app/types';
|
||||
import { store } from 'app/store/store';
|
||||
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { ThrottlingErrorMessage } from './components/ThrottlingErrorMessage';
|
||||
@@ -37,30 +53,14 @@ import {
|
||||
GetLogEventsRequest,
|
||||
GetLogGroupFieldsRequest,
|
||||
GetLogGroupFieldsResponse,
|
||||
isCloudWatchLogsQuery,
|
||||
LogAction,
|
||||
MetricQuery,
|
||||
MetricRequest,
|
||||
TSDBResponse,
|
||||
isCloudWatchLogsQuery,
|
||||
} from './types';
|
||||
import { from, Observable, of, merge, zip } from 'rxjs';
|
||||
import {
|
||||
catchError,
|
||||
finalize,
|
||||
map,
|
||||
mergeMap,
|
||||
tap,
|
||||
concatMap,
|
||||
scan,
|
||||
share,
|
||||
repeat,
|
||||
takeWhile,
|
||||
filter,
|
||||
} from 'rxjs/operators';
|
||||
import { CloudWatchLanguageProvider } from './language_provider';
|
||||
|
||||
import { VariableWithMultiSupport } from 'app/features/variables/types';
|
||||
import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider';
|
||||
import { AwsUrl, encodeUrl } from './aws_url';
|
||||
import { increasingInterval } from './utils/rxjs/increasingInterval';
|
||||
import config from 'app/core/config';
|
||||
@@ -515,7 +515,7 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
|
||||
};
|
||||
};
|
||||
|
||||
get variables() {
|
||||
getVariables() {
|
||||
return this.templateSrv.getVariables().map(v => `$${v.name}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import { catchError, filter, map, tap } from 'rxjs/operators';
|
||||
import addLabelToQuery from './add_label_to_query';
|
||||
import PrometheusLanguageProvider from './language_provider';
|
||||
import { expandRecordingRules } from './language_utils';
|
||||
import PrometheusMetricFindQuery from './metric_find_query';
|
||||
import { getQueryHints } from './query_hints';
|
||||
import { getOriginalMetricName, renderTemplate, transform } from './result_transformer';
|
||||
import {
|
||||
@@ -39,6 +38,8 @@ import {
|
||||
PromScalarData,
|
||||
PromVectorData,
|
||||
} from './types';
|
||||
import { PrometheusVariableSupport } from './variables';
|
||||
import PrometheusMetricFindQuery from './metric_find_query';
|
||||
|
||||
export const ANNOTATION_QUERY_STEP_DEFAULT = '60s';
|
||||
|
||||
@@ -78,6 +79,8 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
||||
this.languageProvider = new PrometheusLanguageProvider(this);
|
||||
this.lookupsDisabled = instanceSettings.jsonData.disableMetricsLookup ?? false;
|
||||
this.customQueryParameters = new URLSearchParams(instanceSettings.jsonData.customQueryParameters);
|
||||
|
||||
this.variables = new PrometheusVariableSupport(this, this.templateSrv, this.timeSrv);
|
||||
}
|
||||
|
||||
init = () => {
|
||||
|
||||
56
public/app/plugins/datasource/prometheus/variables.ts
Normal file
56
public/app/plugins/datasource/prometheus/variables.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { from, Observable, of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import {
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
rangeUtil,
|
||||
StandardVariableQuery,
|
||||
StandardVariableSupport,
|
||||
} from '@grafana/data';
|
||||
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||
|
||||
import { PrometheusDatasource } from './datasource';
|
||||
import { PromQuery } from './types';
|
||||
import PrometheusMetricFindQuery from './metric_find_query';
|
||||
import { getTimeSrv, TimeSrv } from '../../../features/dashboard/services/TimeSrv';
|
||||
|
||||
export class PrometheusVariableSupport extends StandardVariableSupport<PrometheusDatasource> {
|
||||
constructor(
|
||||
private readonly datasource: PrometheusDatasource,
|
||||
private readonly templateSrv: TemplateSrv = getTemplateSrv(),
|
||||
private readonly timeSrv: TimeSrv = getTimeSrv()
|
||||
) {
|
||||
super();
|
||||
this.query = this.query.bind(this);
|
||||
}
|
||||
|
||||
query(request: DataQueryRequest<PromQuery>): Observable<DataQueryResponse> {
|
||||
const query = request.targets[0].expr;
|
||||
if (!query) {
|
||||
return of({ data: [] });
|
||||
}
|
||||
|
||||
const scopedVars = {
|
||||
...request.scopedVars,
|
||||
__interval: { text: this.datasource.interval, value: this.datasource.interval },
|
||||
__interval_ms: {
|
||||
text: rangeUtil.intervalToMs(this.datasource.interval),
|
||||
value: rangeUtil.intervalToMs(this.datasource.interval),
|
||||
},
|
||||
...this.datasource.getRangeScopedVars(this.timeSrv.timeRange()),
|
||||
};
|
||||
|
||||
const interpolated = this.templateSrv.replace(query, scopedVars, this.datasource.interpolateQueryExpr);
|
||||
const metricFindQuery = new PrometheusMetricFindQuery(this.datasource, interpolated);
|
||||
const metricFindStream = from(metricFindQuery.process());
|
||||
|
||||
return metricFindStream.pipe(map(results => ({ data: results })));
|
||||
}
|
||||
|
||||
toDataQuery(query: StandardVariableQuery): PromQuery {
|
||||
return {
|
||||
refId: 'PrometheusDatasource-VariableQuery',
|
||||
expr: query.query,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import set from 'lodash/set';
|
||||
import { from, merge, Observable, of } from 'rxjs';
|
||||
import { delay, map } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
AnnotationEvent,
|
||||
ArrayDataFrame,
|
||||
arrowTableToDataFrame,
|
||||
base64StringToArrowTable,
|
||||
@@ -10,28 +13,25 @@ import {
|
||||
DataQueryResponse,
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
LoadingState,
|
||||
MetricFindValue,
|
||||
TableData,
|
||||
TimeSeries,
|
||||
TimeRange,
|
||||
DataTopic,
|
||||
AnnotationEvent,
|
||||
LiveChannelScope,
|
||||
LoadingState,
|
||||
TableData,
|
||||
TimeRange,
|
||||
TimeSeries,
|
||||
} from '@grafana/data';
|
||||
import { Scenario, TestDataQuery } from './types';
|
||||
import {
|
||||
getBackendSrv,
|
||||
toDataQueryError,
|
||||
getLiveMeasurementsObserver,
|
||||
getTemplateSrv,
|
||||
TemplateSrv,
|
||||
getLiveMeasurementsObserver,
|
||||
toDataQueryError,
|
||||
} from '@grafana/runtime';
|
||||
import { queryMetricTree } from './metricTree';
|
||||
import { from, merge, Observable, of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { runStream } from './runStreams';
|
||||
import { getSearchFilterScopedVar } from 'app/features/variables/utils';
|
||||
import { TestDataVariableSupport } from './variables';
|
||||
|
||||
type TestData = TimeSeries | TableData;
|
||||
|
||||
@@ -43,6 +43,7 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
|
||||
private readonly templateSrv: TemplateSrv = getTemplateSrv()
|
||||
) {
|
||||
super(instanceSettings);
|
||||
this.variables = new TestDataVariableSupport();
|
||||
}
|
||||
|
||||
query(options: DataQueryRequest<TestDataQuery>): Observable<DataQueryResponse> {
|
||||
@@ -71,6 +72,9 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
|
||||
case 'annotations':
|
||||
streams.push(this.annotationDataTopicTest(target, options));
|
||||
break;
|
||||
case 'variables-query':
|
||||
streams.push(this.variablesQuery(target, options));
|
||||
break;
|
||||
default:
|
||||
queries.push({
|
||||
...target,
|
||||
@@ -188,18 +192,17 @@ export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
|
||||
return this.scenariosCache;
|
||||
}
|
||||
|
||||
metricFindQuery(query: string, options: any) {
|
||||
return new Promise<MetricFindValue[]>((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
variablesQuery(target: TestDataQuery, options: DataQueryRequest<TestDataQuery>): Observable<DataQueryResponse> {
|
||||
const query = target.stringInput;
|
||||
const interpolatedQuery = this.templateSrv.replace(
|
||||
query,
|
||||
getSearchFilterScopedVar({ query, wildcardChar: '*', options })
|
||||
getSearchFilterScopedVar({ query, wildcardChar: '*', options: options.scopedVars })
|
||||
);
|
||||
const children = queryMetricTree(interpolatedQuery);
|
||||
const items = children.map(item => ({ value: item.name, text: item.name }));
|
||||
resolve(items);
|
||||
}, 100);
|
||||
});
|
||||
const dataFrame = new ArrayDataFrame(items);
|
||||
|
||||
return of({ data: [dataFrame] }).pipe(delay(100));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
16
public/app/plugins/datasource/testdata/variables.ts
vendored
Normal file
16
public/app/plugins/datasource/testdata/variables.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import { StandardVariableQuery, StandardVariableSupport } from '@grafana/data';
|
||||
|
||||
import { TestDataDataSource } from './datasource';
|
||||
import { TestDataQuery } from './types';
|
||||
|
||||
export class TestDataVariableSupport extends StandardVariableSupport<TestDataDataSource> {
|
||||
toDataQuery(query: StandardVariableQuery): TestDataQuery {
|
||||
return {
|
||||
refId: 'TestDataDataSource-QueryVariable',
|
||||
stringInput: query.query,
|
||||
scenarioId: 'variables-query',
|
||||
csvWave: null,
|
||||
points: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user