Commit 112a755e by Hugo Häggmark Committed by GitHub

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
parent 5ae72802
......@@ -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';
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(
(
......
// 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;
}
}
......
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>
);
}
}
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%' }}>
......
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({});
});
});
});
});
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()}
/>
);
}
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);
});
});
});
});
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))
);
}
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;
......
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,
},
......
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
......
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;
}
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[]) {
......
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();
}
if (!dataSource.metricFindQuery) {
return;
}
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)));
}
const datasource = await getDatasourceSrv().get(variableInState.datasource ?? '');
dispatch(upgradeLegacyQueries(identifier, datasource));
// 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);
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 })));
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: definition })));
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';
}
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');
}
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;
}
......
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);
});
});
});
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);
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,
......
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;
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;
......
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);
}
......
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();
......
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 (
......
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();
......
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,10 +186,12 @@ export class CloudMonitoringVariableQueryEditor extends PureComponent<VariableQu
}
insertTemplateVariables(options: any) {
const templateVariables = this.props.templateSrv.getVariables().map((v: any) => ({
name: `$${v.name}`,
value: `$${v.name}`,
}));
const templateVariables = getTemplateSrv()
.getVariables()
.map((v: any) => ({
name: `$${v.name}`,
value: `$${v.name}`,
}));
return [...templateVariables, ...options];
}
......
......@@ -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[];
......
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 }))
);
}
}
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 });
......
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,
LiveChannelEvent,
LiveChannelMessageEvent,
LiveChannelScope,
LoadingState,
LogRowModel,
rangeUtil,
ScopedVars,
TimeRange,
rangeUtil,
DataQueryErrorType,
LiveChannelScope,
LiveChannelEvent,
LiveChannelMessageEvent,
} 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 = () => {
......
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,
};
}
}
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,
DataTopic,
LiveChannelScope,
LoadingState,
MetricFindValue,
TableData,
TimeSeries,
TimeRange,
DataTopic,
AnnotationEvent,
LiveChannelScope,
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(() => {
const interpolatedQuery = this.templateSrv.replace(
query,
getSearchFilterScopedVar({ query, wildcardChar: '*', options })
);
const children = queryMetricTree(interpolatedQuery);
const items = children.map(item => ({ value: item.name, text: item.name }));
resolve(items);
}, 100);
});
variablesQuery(target: TestDataQuery, options: DataQueryRequest<TestDataQuery>): Observable<DataQueryResponse> {
const query = target.stringInput;
const interpolatedQuery = this.templateSrv.replace(
query,
getSearchFilterScopedVar({ query, wildcardChar: '*', options: options.scopedVars })
);
const children = queryMetricTree(interpolatedQuery);
const items = children.map(item => ({ value: item.name, text: item.name }));
const dataFrame = new ArrayDataFrame(items);
return of({ data: [dataFrame] }).pipe(delay(100));
}
}
......
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: [],
};
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment