mirror of https://github.com/grafana/grafana
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 commentspull/29212/head
parent
5ae7280249
commit
112a755e18
@ -0,0 +1,99 @@ |
||||
import { ComponentType } from 'react'; |
||||
import { Observable } from 'rxjs'; |
||||
|
||||
import { |
||||
DataQuery, |
||||
DataQueryRequest, |
||||
DataQueryResponse, |
||||
DataSourceApi, |
||||
DataSourceJsonData, |
||||
DataSourceOptionsType, |
||||
DataSourceQueryType, |
||||
QueryEditorProps, |
||||
} from './datasource'; |
||||
|
||||
/** |
||||
* Enum with the different variable support types |
||||
* |
||||
* @alpha -- experimental |
||||
*/ |
||||
export enum VariableSupportType { |
||||
Legacy = 'legacy', |
||||
Standard = 'standard', |
||||
Custom = 'custom', |
||||
Datasource = 'datasource', |
||||
} |
||||
|
||||
/** |
||||
* Base class for VariableSupport classes |
||||
* |
||||
* @alpha -- experimental |
||||
*/ |
||||
export abstract class VariableSupportBase< |
||||
DSType extends DataSourceApi<TQuery, TOptions>, |
||||
TQuery extends DataQuery = DataSourceQueryType<DSType>, |
||||
TOptions extends DataSourceJsonData = DataSourceOptionsType<DSType> |
||||
> { |
||||
abstract getType(): VariableSupportType; |
||||
} |
||||
|
||||
/** |
||||
* Extend this class in a data source plugin to use the standard query editor for Query variables |
||||
* |
||||
* @alpha -- experimental |
||||
*/ |
||||
export abstract class StandardVariableSupport< |
||||
DSType extends DataSourceApi<TQuery, TOptions>, |
||||
TQuery extends DataQuery = DataSourceQueryType<DSType>, |
||||
TOptions extends DataSourceJsonData = DataSourceOptionsType<DSType> |
||||
> extends VariableSupportBase<DSType, TQuery, TOptions> { |
||||
getType(): VariableSupportType { |
||||
return VariableSupportType.Standard; |
||||
} |
||||
|
||||
abstract toDataQuery(query: StandardVariableQuery): TQuery; |
||||
query?(request: DataQueryRequest<TQuery>): Observable<DataQueryResponse>; |
||||
} |
||||
|
||||
/** |
||||
* Extend this class in a data source plugin to use a customized query editor for Query variables |
||||
* |
||||
* @alpha -- experimental |
||||
*/ |
||||
export abstract class CustomVariableSupport< |
||||
DSType extends DataSourceApi<TQuery, TOptions>, |
||||
VariableQuery extends DataQuery = any, |
||||
TQuery extends DataQuery = DataSourceQueryType<DSType>, |
||||
TOptions extends DataSourceJsonData = DataSourceOptionsType<DSType> |
||||
> extends VariableSupportBase<DSType, TQuery, TOptions> { |
||||
getType(): VariableSupportType { |
||||
return VariableSupportType.Custom; |
||||
} |
||||
|
||||
abstract editor: ComponentType<QueryEditorProps<DSType, TQuery, TOptions, VariableQuery>>; |
||||
abstract query(request: DataQueryRequest<VariableQuery>): Observable<DataQueryResponse>; |
||||
} |
||||
|
||||
/** |
||||
* Extend this class in a data source plugin to use the query editor in the data source plugin for Query variables |
||||
* |
||||
* @alpha -- experimental |
||||
*/ |
||||
export abstract class DataSourceVariableSupport< |
||||
DSType extends DataSourceApi<TQuery, TOptions>, |
||||
TQuery extends DataQuery = DataSourceQueryType<DSType>, |
||||
TOptions extends DataSourceJsonData = DataSourceOptionsType<DSType> |
||||
> extends VariableSupportBase<DSType, TQuery, TOptions> { |
||||
getType(): VariableSupportType { |
||||
return VariableSupportType.Datasource; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Defines the standard DatQuery used by data source plugins that implement StandardVariableSupport |
||||
* |
||||
* @alpha -- experimental |
||||
*/ |
||||
export interface StandardVariableQuery extends DataQuery { |
||||
query: string; |
||||
} |
||||
@ -1,46 +0,0 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { VariableQueryProps } from 'app/types/plugins'; |
||||
import { selectors } from '@grafana/e2e-selectors'; |
||||
|
||||
export default class DefaultVariableQueryEditor extends PureComponent<VariableQueryProps, any> { |
||||
constructor(props: VariableQueryProps) { |
||||
super(props); |
||||
this.state = { value: props.query }; |
||||
} |
||||
|
||||
onChange = (event: React.FormEvent<HTMLTextAreaElement>) => { |
||||
this.setState({ value: event.currentTarget.value }); |
||||
}; |
||||
|
||||
onBlur = (event: React.FormEvent<HTMLTextAreaElement>) => { |
||||
this.props.onChange(event.currentTarget.value, event.currentTarget.value); |
||||
}; |
||||
|
||||
getLineCount() { |
||||
const { value } = this.state; |
||||
|
||||
if (typeof value === 'string') { |
||||
return value.split('\n').length; |
||||
} |
||||
|
||||
return 1; |
||||
} |
||||
|
||||
render() { |
||||
return ( |
||||
<div className="gf-form"> |
||||
<span className="gf-form-label width-10">Query</span> |
||||
<textarea |
||||
rows={this.getLineCount()} |
||||
className="gf-form-input" |
||||
value={this.state.value} |
||||
onChange={this.onChange} |
||||
onBlur={this.onBlur} |
||||
placeholder="metric name or tags query" |
||||
required |
||||
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput} |
||||
/> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,62 @@ |
||||
import React, { FC, useCallback, useState } from 'react'; |
||||
import { selectors } from '@grafana/e2e-selectors'; |
||||
|
||||
import { VariableQueryProps } from 'app/types/plugins'; |
||||
import { InlineField, TextArea, useStyles } from '@grafana/ui'; |
||||
import { GrafanaTheme } from '@grafana/data'; |
||||
import { css } from 'emotion'; |
||||
|
||||
export const LEGACY_VARIABLE_QUERY_EDITOR_NAME = 'Grafana-LegacyVariableQueryEditor'; |
||||
|
||||
export const LegacyVariableQueryEditor: FC<VariableQueryProps> = ({ onChange, query }) => { |
||||
const styles = useStyles(getStyles); |
||||
const [value, setValue] = useState(query); |
||||
const onValueChange = useCallback( |
||||
(event: React.FormEvent<HTMLTextAreaElement>) => { |
||||
setValue(event.currentTarget.value); |
||||
}, |
||||
[onChange] |
||||
); |
||||
const onBlur = useCallback( |
||||
(event: React.FormEvent<HTMLTextAreaElement>) => { |
||||
onChange(event.currentTarget.value, event.currentTarget.value); |
||||
}, |
||||
[onChange] |
||||
); |
||||
|
||||
return ( |
||||
<div className="gf-form"> |
||||
<InlineField label="Query" labelWidth={20} grow={false} className={styles.inlineFieldOverride}> |
||||
<span hidden /> |
||||
</InlineField> |
||||
<TextArea |
||||
rows={getLineCount(value)} |
||||
className="gf-form-input" |
||||
value={value} |
||||
onChange={onValueChange} |
||||
onBlur={onBlur} |
||||
placeholder="metric name or tags query" |
||||
required |
||||
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput} |
||||
/> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
function getStyles(theme: GrafanaTheme) { |
||||
return { |
||||
inlineFieldOverride: css` |
||||
margin: 0; |
||||
`,
|
||||
}; |
||||
} |
||||
|
||||
LegacyVariableQueryEditor.displayName = LEGACY_VARIABLE_QUERY_EDITOR_NAME; |
||||
|
||||
const getLineCount = (value: any) => { |
||||
if (value && typeof value === 'string') { |
||||
return value.split('\n').length; |
||||
} |
||||
|
||||
return 1; |
||||
}; |
||||
@ -0,0 +1,103 @@ |
||||
import { VariableSupportType } from '@grafana/data'; |
||||
import { getVariableQueryEditor, StandardVariableQueryEditor } from './getVariableQueryEditor'; |
||||
import { LegacyVariableQueryEditor } from './LegacyVariableQueryEditor'; |
||||
|
||||
describe('getVariableQueryEditor', () => { |
||||
describe('happy cases', () => { |
||||
describe('when called with a data source with custom variable support', () => { |
||||
it('then it should return correct editor', async () => { |
||||
const editor: any = StandardVariableQueryEditor; |
||||
const datasource: any = { |
||||
variables: { getType: () => VariableSupportType.Custom, query: () => undefined, editor }, |
||||
}; |
||||
|
||||
const result = await getVariableQueryEditor(datasource); |
||||
|
||||
expect(result).toBe(editor); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a data source with standard variable support', () => { |
||||
it('then it should return correct editor', async () => { |
||||
const editor: any = StandardVariableQueryEditor; |
||||
const datasource: any = { |
||||
variables: { getType: () => VariableSupportType.Standard, toDataQuery: () => undefined }, |
||||
}; |
||||
|
||||
const result = await getVariableQueryEditor(datasource); |
||||
|
||||
expect(result).toBe(editor); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a data source with datasource variable support', () => { |
||||
it('then it should return correct editor', async () => { |
||||
const editor: any = StandardVariableQueryEditor; |
||||
const plugin = { components: { QueryEditor: editor } }; |
||||
const importDataSourcePluginFunc = jest.fn().mockResolvedValue(plugin); |
||||
const datasource: any = { variables: { getType: () => VariableSupportType.Datasource }, meta: {} }; |
||||
|
||||
const result = await getVariableQueryEditor(datasource, importDataSourcePluginFunc); |
||||
|
||||
expect(result).toBe(editor); |
||||
expect(importDataSourcePluginFunc).toHaveBeenCalledTimes(1); |
||||
expect(importDataSourcePluginFunc).toHaveBeenCalledWith({}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a data source with legacy variable support', () => { |
||||
it('then it should return correct editor', async () => { |
||||
const editor: any = StandardVariableQueryEditor; |
||||
const plugin = { components: { VariableQueryEditor: editor } }; |
||||
const importDataSourcePluginFunc = jest.fn().mockResolvedValue(plugin); |
||||
const datasource: any = { metricFindQuery: () => undefined, meta: {} }; |
||||
|
||||
const result = await getVariableQueryEditor(datasource, importDataSourcePluginFunc); |
||||
|
||||
expect(result).toBe(editor); |
||||
expect(importDataSourcePluginFunc).toHaveBeenCalledTimes(1); |
||||
expect(importDataSourcePluginFunc).toHaveBeenCalledWith({}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('negative cases', () => { |
||||
describe('when variable support is not recognized', () => { |
||||
it('then it should return null', async () => { |
||||
const datasource: any = {}; |
||||
|
||||
const result = await getVariableQueryEditor(datasource); |
||||
|
||||
expect(result).toBeNull(); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a data source with datasource variable support but missing QueryEditor', () => { |
||||
it('then it should return throw', async () => { |
||||
const plugin = { components: {} }; |
||||
const importDataSourcePluginFunc = jest.fn().mockResolvedValue(plugin); |
||||
const datasource: any = { variables: { getType: () => VariableSupportType.Datasource }, meta: {} }; |
||||
|
||||
await expect(getVariableQueryEditor(datasource, importDataSourcePluginFunc)).rejects.toThrow( |
||||
new Error('Missing QueryEditor in plugin definition.') |
||||
); |
||||
expect(importDataSourcePluginFunc).toHaveBeenCalledTimes(1); |
||||
expect(importDataSourcePluginFunc).toHaveBeenCalledWith({}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a data source with legacy variable support but missing VariableQueryEditor', () => { |
||||
it('then it should return LegacyVariableQueryEditor', async () => { |
||||
const plugin = { components: {} }; |
||||
const importDataSourcePluginFunc = jest.fn().mockResolvedValue(plugin); |
||||
const datasource: any = { metricFindQuery: () => undefined, meta: {} }; |
||||
|
||||
const result = await getVariableQueryEditor(datasource, importDataSourcePluginFunc); |
||||
|
||||
expect(result).toBe(LegacyVariableQueryEditor); |
||||
expect(importDataSourcePluginFunc).toHaveBeenCalledTimes(1); |
||||
expect(importDataSourcePluginFunc).toHaveBeenCalledWith({}); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,72 @@ |
||||
import React, { useCallback } from 'react'; |
||||
import { DataQuery, DataSourceApi, DataSourceJsonData, QueryEditorProps, StandardVariableQuery } from '@grafana/data'; |
||||
import { getTemplateSrv } from '@grafana/runtime'; |
||||
|
||||
import { LegacyVariableQueryEditor } from './LegacyVariableQueryEditor'; |
||||
import { |
||||
hasCustomVariableSupport, |
||||
hasDatasourceVariableSupport, |
||||
hasLegacyVariableSupport, |
||||
hasStandardVariableSupport, |
||||
} from '../guard'; |
||||
import { importDataSourcePlugin } from '../../plugins/plugin_loader'; |
||||
import { VariableQueryEditorType } from '../types'; |
||||
|
||||
export async function getVariableQueryEditor< |
||||
TQuery extends DataQuery = DataQuery, |
||||
TOptions extends DataSourceJsonData = DataSourceJsonData, |
||||
VariableQuery extends DataQuery = TQuery |
||||
>( |
||||
datasource: DataSourceApi<TQuery, TOptions>, |
||||
importDataSourcePluginFunc = importDataSourcePlugin |
||||
): Promise<VariableQueryEditorType> { |
||||
if (hasCustomVariableSupport(datasource)) { |
||||
return datasource.variables.editor; |
||||
} |
||||
|
||||
if (hasDatasourceVariableSupport(datasource)) { |
||||
const dsPlugin = await importDataSourcePluginFunc(datasource.meta!); |
||||
|
||||
if (!dsPlugin.components.QueryEditor) { |
||||
throw new Error('Missing QueryEditor in plugin definition.'); |
||||
} |
||||
|
||||
return dsPlugin.components.QueryEditor ?? null; |
||||
} |
||||
|
||||
if (hasStandardVariableSupport(datasource)) { |
||||
return StandardVariableQueryEditor; |
||||
} |
||||
|
||||
if (hasLegacyVariableSupport(datasource)) { |
||||
const dsPlugin = await importDataSourcePluginFunc(datasource.meta!); |
||||
return dsPlugin.components.VariableQueryEditor ?? LegacyVariableQueryEditor; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
export function StandardVariableQueryEditor< |
||||
TQuery extends DataQuery = DataQuery, |
||||
TOptions extends DataSourceJsonData = DataSourceJsonData |
||||
>({ |
||||
datasource: propsDatasource, |
||||
query: propsQuery, |
||||
onChange: propsOnChange, |
||||
}: QueryEditorProps<any, TQuery, TOptions, StandardVariableQuery>) { |
||||
const onChange = useCallback( |
||||
(query: any) => { |
||||
propsOnChange({ refId: 'StandardVariableQuery', query }); |
||||
}, |
||||
[propsOnChange] |
||||
); |
||||
|
||||
return ( |
||||
<LegacyVariableQueryEditor |
||||
query={propsQuery.query} |
||||
onChange={onChange} |
||||
datasource={propsDatasource} |
||||
templateSrv={getTemplateSrv()} |
||||
/> |
||||
); |
||||
} |
||||
@ -0,0 +1,243 @@ |
||||
import { |
||||
hasCustomVariableSupport, |
||||
hasDatasourceVariableSupport, |
||||
hasLegacyVariableSupport, |
||||
hasStandardVariableSupport, |
||||
isLegacyQueryEditor, |
||||
isQueryEditor, |
||||
} from './guard'; |
||||
import { LegacyVariableQueryEditor } from './editor/LegacyVariableQueryEditor'; |
||||
import { StandardVariableQueryEditor } from './editor/getVariableQueryEditor'; |
||||
import { VariableSupportType } from '@grafana/data'; |
||||
|
||||
describe('type guards', () => { |
||||
describe('hasLegacyVariableSupport', () => { |
||||
describe('when called with a legacy data source', () => { |
||||
it('should return true', () => { |
||||
const datasource: any = { metricFindQuery: () => undefined }; |
||||
expect(hasLegacyVariableSupport(datasource)).toBe(true); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with data source without metricFindQuery function', () => { |
||||
it('should return false', () => { |
||||
const datasource: any = {}; |
||||
expect(hasLegacyVariableSupport(datasource)).toBe(false); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a legacy data source with variable support', () => { |
||||
it('should return false', () => { |
||||
const datasource: any = { metricFindQuery: () => undefined, variables: {} }; |
||||
expect(hasLegacyVariableSupport(datasource)).toBe(false); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('hasStandardVariableSupport', () => { |
||||
describe('when called with a data source with standard variable support', () => { |
||||
it('should return true', () => { |
||||
const datasource: any = { |
||||
metricFindQuery: () => undefined, |
||||
variables: { getType: () => VariableSupportType.Standard, toDataQuery: () => undefined }, |
||||
}; |
||||
expect(hasStandardVariableSupport(datasource)).toBe(true); |
||||
}); |
||||
|
||||
describe('and with a custom query', () => { |
||||
it('should return true', () => { |
||||
const datasource: any = { |
||||
metricFindQuery: () => undefined, |
||||
variables: { |
||||
getType: () => VariableSupportType.Standard, |
||||
toDataQuery: () => undefined, |
||||
query: () => undefined, |
||||
}, |
||||
}; |
||||
expect(hasStandardVariableSupport(datasource)).toBe(true); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a data source with partial standard variable support', () => { |
||||
it('should return false', () => { |
||||
const datasource: any = { |
||||
metricFindQuery: () => undefined, |
||||
variables: { getType: () => VariableSupportType.Standard, query: () => undefined }, |
||||
}; |
||||
expect(hasStandardVariableSupport(datasource)).toBe(false); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a data source without standard variable support', () => { |
||||
it('should return false', () => { |
||||
const datasource: any = { metricFindQuery: () => undefined }; |
||||
expect(hasStandardVariableSupport(datasource)).toBe(false); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('hasCustomVariableSupport', () => { |
||||
describe('when called with a data source with custom variable support', () => { |
||||
it('should return true', () => { |
||||
const datasource: any = { |
||||
metricFindQuery: () => undefined, |
||||
variables: { getType: () => VariableSupportType.Custom, query: () => undefined, editor: {} }, |
||||
}; |
||||
expect(hasCustomVariableSupport(datasource)).toBe(true); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a data source with custom variable support but without editor', () => { |
||||
it('should return false', () => { |
||||
const datasource: any = { |
||||
metricFindQuery: () => undefined, |
||||
variables: { getType: () => VariableSupportType.Custom, query: () => undefined }, |
||||
}; |
||||
expect(hasCustomVariableSupport(datasource)).toBe(false); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a data source with custom variable support but without query', () => { |
||||
it('should return false', () => { |
||||
const datasource: any = { |
||||
metricFindQuery: () => undefined, |
||||
variables: { getType: () => VariableSupportType.Custom, editor: {} }, |
||||
}; |
||||
expect(hasCustomVariableSupport(datasource)).toBe(false); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a data source without custom variable support', () => { |
||||
it('should return false', () => { |
||||
const datasource: any = { metricFindQuery: () => undefined }; |
||||
expect(hasCustomVariableSupport(datasource)).toBe(false); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('hasDatasourceVariableSupport', () => { |
||||
describe('when called with a data source with datasource variable support', () => { |
||||
it('should return true', () => { |
||||
const datasource: any = { |
||||
metricFindQuery: () => undefined, |
||||
variables: { getType: () => VariableSupportType.Datasource }, |
||||
}; |
||||
expect(hasDatasourceVariableSupport(datasource)).toBe(true); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a data source without datasource variable support', () => { |
||||
it('should return false', () => { |
||||
const datasource: any = { metricFindQuery: () => undefined }; |
||||
expect(hasDatasourceVariableSupport(datasource)).toBe(false); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('isLegacyQueryEditor', () => { |
||||
describe('happy cases', () => { |
||||
describe('when called with a legacy query editor but without a legacy data source', () => { |
||||
it('then is should return true', () => { |
||||
const component: any = LegacyVariableQueryEditor; |
||||
const datasource: any = {}; |
||||
|
||||
expect(isLegacyQueryEditor(component, datasource)).toBe(true); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a legacy data source but without a legacy query editor', () => { |
||||
it('then is should return true', () => { |
||||
const component: any = StandardVariableQueryEditor; |
||||
const datasource: any = { metricFindQuery: () => undefined }; |
||||
|
||||
expect(isLegacyQueryEditor(component, datasource)).toBe(true); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('negative cases', () => { |
||||
describe('when called without component', () => { |
||||
it('then is should return false', () => { |
||||
const component: any = null; |
||||
const datasource: any = { metricFindQuery: () => undefined }; |
||||
|
||||
expect(isLegacyQueryEditor(component, datasource)).toBe(false); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called without a legacy query editor and without a legacy data source', () => { |
||||
it('then is should return false', () => { |
||||
const component: any = StandardVariableQueryEditor; |
||||
const datasource: any = {}; |
||||
|
||||
expect(isLegacyQueryEditor(component, datasource)).toBe(false); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('isQueryEditor', () => { |
||||
describe('happy cases', () => { |
||||
describe('when called without a legacy editor and with a data source with standard variable support', () => { |
||||
it('then is should return true', () => { |
||||
const component: any = StandardVariableQueryEditor; |
||||
const datasource: any = { |
||||
variables: { getType: () => VariableSupportType.Standard, toDataQuery: () => undefined }, |
||||
}; |
||||
|
||||
expect(isQueryEditor(component, datasource)).toBe(true); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called without a legacy editor and with a data source with custom variable support', () => { |
||||
it('then is should return true', () => { |
||||
const component: any = StandardVariableQueryEditor; |
||||
const datasource: any = { |
||||
variables: { getType: () => VariableSupportType.Custom, query: () => undefined, editor: {} }, |
||||
}; |
||||
|
||||
expect(isQueryEditor(component, datasource)).toBe(true); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called without a legacy editor and with a data source with datasource variable support', () => { |
||||
it('then is should return true', () => { |
||||
const component: any = StandardVariableQueryEditor; |
||||
const datasource: any = { variables: { getType: () => VariableSupportType.Datasource } }; |
||||
|
||||
expect(isQueryEditor(component, datasource)).toBe(true); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('negative cases', () => { |
||||
describe('when called without component', () => { |
||||
it('then is should return false', () => { |
||||
const component: any = null; |
||||
const datasource: any = { metricFindQuery: () => undefined }; |
||||
|
||||
expect(isQueryEditor(component, datasource)).toBe(false); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a legacy query editor', () => { |
||||
it('then is should return false', () => { |
||||
const component: any = LegacyVariableQueryEditor; |
||||
const datasource: any = { variables: { getType: () => VariableSupportType.Datasource } }; |
||||
|
||||
expect(isQueryEditor(component, datasource)).toBe(false); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called without a legacy query editor but with a legacy data source', () => { |
||||
it('then is should return false', () => { |
||||
const component: any = StandardVariableQueryEditor; |
||||
const datasource: any = { metricFindQuery: () => undefined }; |
||||
|
||||
expect(isQueryEditor(component, datasource)).toBe(false); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,396 @@ |
||||
import { of, throwError } from 'rxjs'; |
||||
import { DefaultTimeRange, LoadingState, VariableSupportType } from '@grafana/data'; |
||||
import { delay } from 'rxjs/operators'; |
||||
|
||||
import { UpdateOptionsResults, VariableQueryRunner } from './VariableQueryRunner'; |
||||
import { queryBuilder } from '../shared/testing/builders'; |
||||
import { QueryRunner, QueryRunners } from './queryRunners'; |
||||
import { toVariableIdentifier, VariableIdentifier } from '../state/types'; |
||||
import { QueryVariableModel } from '../types'; |
||||
import { updateVariableOptions, updateVariableTags } from './reducer'; |
||||
|
||||
type DoneCallback = { |
||||
(...args: any[]): any; |
||||
fail(error?: string | { message: string }): any; |
||||
}; |
||||
|
||||
function expectOnResults(args: { |
||||
runner: VariableQueryRunner; |
||||
identifier: VariableIdentifier; |
||||
done: DoneCallback; |
||||
expect: (results: UpdateOptionsResults[]) => void; |
||||
}) { |
||||
const { runner, identifier, done, expect: expectCallback } = args; |
||||
const results: UpdateOptionsResults[] = []; |
||||
const subscription = runner.getResponse(identifier).subscribe({ |
||||
next: value => { |
||||
results.push(value); |
||||
if (value.state === LoadingState.Done || value.state === LoadingState.Error) { |
||||
try { |
||||
expectCallback(results); |
||||
subscription.unsubscribe(); |
||||
done(); |
||||
} catch (err) { |
||||
subscription.unsubscribe(); |
||||
done.fail(err); |
||||
} |
||||
} |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
function getTestContext(variable?: QueryVariableModel) { |
||||
variable = |
||||
variable ?? |
||||
queryBuilder() |
||||
.withId('query') |
||||
.build(); |
||||
const getTimeSrv = jest.fn().mockReturnValue({ |
||||
timeRange: jest.fn().mockReturnValue(DefaultTimeRange), |
||||
}); |
||||
const datasource: any = { metricFindQuery: jest.fn().mockResolvedValue([]) }; |
||||
const identifier = toVariableIdentifier(variable); |
||||
const searchFilter = undefined; |
||||
const getTemplatedRegex = jest.fn().mockReturnValue('getTemplatedRegex result'); |
||||
const dispatch = jest.fn().mockResolvedValue({}); |
||||
const getState = jest.fn().mockReturnValue({ |
||||
templating: { |
||||
transaction: { |
||||
uid: '0123456789', |
||||
}, |
||||
}, |
||||
variables: { |
||||
[variable.id]: variable, |
||||
}, |
||||
}); |
||||
const queryRunner: QueryRunner = { |
||||
type: VariableSupportType.Standard, |
||||
canRun: jest.fn().mockReturnValue(true), |
||||
getTarget: jest.fn().mockReturnValue({ refId: 'A', query: 'A query' }), |
||||
runRequest: jest.fn().mockReturnValue(of({ series: [], state: LoadingState.Done })), |
||||
}; |
||||
const queryRunners = ({ |
||||
getRunnerForDatasource: jest.fn().mockReturnValue(queryRunner), |
||||
} as unknown) as QueryRunners; |
||||
const getVariable = jest.fn().mockReturnValue(variable); |
||||
const runRequest = jest.fn().mockReturnValue(of({})); |
||||
const runner = new VariableQueryRunner({ |
||||
getTimeSrv, |
||||
getTemplatedRegex, |
||||
dispatch, |
||||
getState, |
||||
getVariable, |
||||
queryRunners, |
||||
runRequest, |
||||
}); |
||||
|
||||
return { |
||||
identifier, |
||||
datasource, |
||||
runner, |
||||
searchFilter, |
||||
getTemplatedRegex, |
||||
dispatch, |
||||
getState, |
||||
queryRunner, |
||||
queryRunners, |
||||
getVariable, |
||||
runRequest, |
||||
variable, |
||||
getTimeSrv, |
||||
}; |
||||
} |
||||
|
||||
describe('VariableQueryRunner', () => { |
||||
describe('happy case', () => { |
||||
it('then it should work as expected', done => { |
||||
const { |
||||
identifier, |
||||
runner, |
||||
datasource, |
||||
getState, |
||||
getVariable, |
||||
queryRunners, |
||||
queryRunner, |
||||
dispatch, |
||||
} = getTestContext(); |
||||
|
||||
expectOnResults({ |
||||
identifier, |
||||
runner, |
||||
expect: results => { |
||||
// verify that the observable works as expected
|
||||
expect(results).toEqual([ |
||||
{ state: LoadingState.Loading, identifier }, |
||||
{ state: LoadingState.Done, identifier }, |
||||
]); |
||||
|
||||
// verify that mocks have been called as expected
|
||||
expect(getState).toHaveBeenCalledTimes(3); |
||||
expect(getVariable).toHaveBeenCalledTimes(1); |
||||
expect(queryRunners.getRunnerForDatasource).toHaveBeenCalledTimes(1); |
||||
expect(queryRunner.getTarget).toHaveBeenCalledTimes(1); |
||||
expect(queryRunner.runRequest).toHaveBeenCalledTimes(1); |
||||
expect(datasource.metricFindQuery).not.toHaveBeenCalled(); |
||||
|
||||
// updateVariableOptions and validateVariableSelectionState
|
||||
expect(dispatch).toHaveBeenCalledTimes(2); |
||||
expect(dispatch.mock.calls[0][0]).toEqual( |
||||
updateVariableOptions({ |
||||
id: 'query', |
||||
type: 'query', |
||||
data: { results: [], templatedRegex: 'getTemplatedRegex result' }, |
||||
}) |
||||
); |
||||
}, |
||||
done, |
||||
}); |
||||
|
||||
runner.queueRequest({ identifier, datasource }); |
||||
}); |
||||
}); |
||||
|
||||
describe('tags case', () => { |
||||
it('then it should work as expected', done => { |
||||
const variable = queryBuilder() |
||||
.withId('query') |
||||
.withTags(true) |
||||
.withTagsQuery('A tags query') |
||||
.build(); |
||||
const { |
||||
identifier, |
||||
runner, |
||||
datasource, |
||||
getState, |
||||
getVariable, |
||||
queryRunners, |
||||
queryRunner, |
||||
dispatch, |
||||
} = getTestContext(variable); |
||||
|
||||
expectOnResults({ |
||||
identifier, |
||||
runner, |
||||
expect: results => { |
||||
// verify that the observable works as expected
|
||||
expect(results).toEqual([ |
||||
{ state: LoadingState.Loading, identifier }, |
||||
{ state: LoadingState.Done, identifier }, |
||||
]); |
||||
|
||||
// verify that mocks have been called as expected
|
||||
expect(getState).toHaveBeenCalledTimes(3); |
||||
expect(getVariable).toHaveBeenCalledTimes(1); |
||||
expect(queryRunners.getRunnerForDatasource).toHaveBeenCalledTimes(1); |
||||
expect(queryRunner.getTarget).toHaveBeenCalledTimes(1); |
||||
expect(queryRunner.runRequest).toHaveBeenCalledTimes(1); |
||||
expect(datasource.metricFindQuery).toHaveBeenCalledTimes(1); |
||||
|
||||
// updateVariableOptions, updateVariableTags and validateVariableSelectionState
|
||||
expect(dispatch).toHaveBeenCalledTimes(3); |
||||
expect(dispatch.mock.calls[0][0]).toEqual( |
||||
updateVariableOptions({ |
||||
id: 'query', |
||||
type: 'query', |
||||
data: { results: [], templatedRegex: 'getTemplatedRegex result' }, |
||||
}) |
||||
); |
||||
expect(dispatch.mock.calls[1][0]).toEqual(updateVariableTags({ id: 'query', type: 'query', data: [] })); |
||||
}, |
||||
done, |
||||
}); |
||||
|
||||
runner.queueRequest({ identifier, datasource }); |
||||
}); |
||||
}); |
||||
|
||||
describe('error cases', () => { |
||||
describe('queryRunners.getRunnerForDatasource throws', () => { |
||||
it('then it should work as expected', done => { |
||||
const { |
||||
identifier, |
||||
runner, |
||||
datasource, |
||||
getState, |
||||
getVariable, |
||||
queryRunners, |
||||
queryRunner, |
||||
dispatch, |
||||
} = getTestContext(); |
||||
|
||||
queryRunners.getRunnerForDatasource = jest.fn().mockImplementation(() => { |
||||
throw new Error('getRunnerForDatasource error'); |
||||
}); |
||||
|
||||
expectOnResults({ |
||||
identifier, |
||||
runner, |
||||
expect: results => { |
||||
// verify that the observable works as expected
|
||||
expect(results).toEqual([ |
||||
{ state: LoadingState.Loading, identifier }, |
||||
{ state: LoadingState.Error, identifier, error: new Error('getRunnerForDatasource error') }, |
||||
]); |
||||
|
||||
// verify that mocks have been called as expected
|
||||
expect(getState).toHaveBeenCalledTimes(2); |
||||
expect(getVariable).toHaveBeenCalledTimes(1); |
||||
expect(queryRunners.getRunnerForDatasource).toHaveBeenCalledTimes(1); |
||||
expect(queryRunner.getTarget).not.toHaveBeenCalled(); |
||||
expect(queryRunner.runRequest).not.toHaveBeenCalled(); |
||||
expect(datasource.metricFindQuery).not.toHaveBeenCalled(); |
||||
expect(dispatch).not.toHaveBeenCalled(); |
||||
}, |
||||
done, |
||||
}); |
||||
|
||||
runner.queueRequest({ identifier, datasource }); |
||||
}); |
||||
}); |
||||
|
||||
describe('runRequest throws', () => { |
||||
it('then it should work as expected', done => { |
||||
const { |
||||
identifier, |
||||
runner, |
||||
datasource, |
||||
getState, |
||||
getVariable, |
||||
queryRunners, |
||||
queryRunner, |
||||
dispatch, |
||||
} = getTestContext(); |
||||
|
||||
queryRunner.runRequest = jest.fn().mockReturnValue(throwError(new Error('runRequest error'))); |
||||
|
||||
expectOnResults({ |
||||
identifier, |
||||
runner, |
||||
expect: results => { |
||||
// verify that the observable works as expected
|
||||
expect(results).toEqual([ |
||||
{ state: LoadingState.Loading, identifier }, |
||||
{ state: LoadingState.Error, identifier, error: new Error('runRequest error') }, |
||||
]); |
||||
|
||||
// verify that mocks have been called as expected
|
||||
expect(getState).toHaveBeenCalledTimes(2); |
||||
expect(getVariable).toHaveBeenCalledTimes(1); |
||||
expect(queryRunners.getRunnerForDatasource).toHaveBeenCalledTimes(1); |
||||
expect(queryRunner.getTarget).toHaveBeenCalledTimes(1); |
||||
expect(queryRunner.runRequest).toHaveBeenCalledTimes(1); |
||||
expect(datasource.metricFindQuery).not.toHaveBeenCalled(); |
||||
expect(dispatch).not.toHaveBeenCalled(); |
||||
}, |
||||
done, |
||||
}); |
||||
|
||||
runner.queueRequest({ identifier, datasource }); |
||||
}); |
||||
}); |
||||
|
||||
describe('metricFindQuery throws', () => { |
||||
it('then it should work as expected', done => { |
||||
const variable = queryBuilder() |
||||
.withId('query') |
||||
.withTags(true) |
||||
.withTagsQuery('A tags query') |
||||
.build(); |
||||
const { |
||||
identifier, |
||||
runner, |
||||
datasource, |
||||
getState, |
||||
getVariable, |
||||
queryRunners, |
||||
queryRunner, |
||||
dispatch, |
||||
} = getTestContext(variable); |
||||
|
||||
datasource.metricFindQuery = jest.fn().mockRejectedValue(new Error('metricFindQuery error')); |
||||
|
||||
expectOnResults({ |
||||
identifier, |
||||
runner, |
||||
expect: results => { |
||||
// verify that the observable works as expected
|
||||
expect(results).toEqual([ |
||||
{ state: LoadingState.Loading, identifier }, |
||||
{ state: LoadingState.Error, identifier, error: new Error('metricFindQuery error') }, |
||||
]); |
||||
|
||||
// verify that mocks have been called as expected
|
||||
expect(getState).toHaveBeenCalledTimes(3); |
||||
expect(getVariable).toHaveBeenCalledTimes(1); |
||||
expect(queryRunners.getRunnerForDatasource).toHaveBeenCalledTimes(1); |
||||
expect(queryRunner.getTarget).toHaveBeenCalledTimes(1); |
||||
expect(queryRunner.runRequest).toHaveBeenCalledTimes(1); |
||||
expect(datasource.metricFindQuery).toHaveBeenCalledTimes(1); |
||||
expect(dispatch).toHaveBeenCalledTimes(1); |
||||
}, |
||||
done, |
||||
}); |
||||
|
||||
runner.queueRequest({ identifier, datasource }); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('cancellation cases', () => { |
||||
describe('long running request is cancelled', () => { |
||||
it('then it should work as expected', done => { |
||||
const { identifier, datasource, runner, queryRunner } = getTestContext(); |
||||
|
||||
queryRunner.runRequest = jest |
||||
.fn() |
||||
.mockReturnValue(of({ series: [], state: LoadingState.Done }).pipe(delay(10000))); |
||||
|
||||
expectOnResults({ |
||||
identifier, |
||||
runner, |
||||
expect: results => { |
||||
// verify that the observable works as expected
|
||||
expect(results).toEqual([ |
||||
{ state: LoadingState.Loading, identifier }, |
||||
{ state: LoadingState.Loading, identifier, cancelled: true }, |
||||
{ state: LoadingState.Done, identifier }, |
||||
]); |
||||
}, |
||||
done, |
||||
}); |
||||
|
||||
runner.queueRequest({ identifier, datasource }); |
||||
runner.cancelRequest(identifier); |
||||
}); |
||||
}); |
||||
|
||||
describe('an identical request is triggered before first request is finished', () => { |
||||
it('then it should work as expected', done => { |
||||
const { identifier, datasource, runner, queryRunner } = getTestContext(); |
||||
|
||||
queryRunner.runRequest = jest |
||||
.fn() |
||||
.mockReturnValueOnce(of({ series: [], state: LoadingState.Done }).pipe(delay(10000))) |
||||
.mockReturnValue(of({ series: [], state: LoadingState.Done })); |
||||
|
||||
expectOnResults({ |
||||
identifier, |
||||
runner, |
||||
expect: results => { |
||||
// verify that the observable works as expected
|
||||
expect(results).toEqual([ |
||||
{ state: LoadingState.Loading, identifier }, |
||||
{ state: LoadingState.Loading, identifier }, |
||||
{ state: LoadingState.Loading, identifier, cancelled: true }, |
||||
{ state: LoadingState.Done, identifier }, |
||||
]); |
||||
}, |
||||
done, |
||||
}); |
||||
|
||||
runner.queueRequest({ identifier, datasource }); |
||||
runner.queueRequest({ identifier, datasource }); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,206 @@ |
||||
import { merge, Observable, of, Subject, throwError, Unsubscribable } from 'rxjs'; |
||||
import { catchError, filter, finalize, first, mergeMap, takeUntil } from 'rxjs/operators'; |
||||
import { |
||||
CoreApp, |
||||
DataQuery, |
||||
DataQueryRequest, |
||||
DataSourceApi, |
||||
DefaultTimeRange, |
||||
LoadingState, |
||||
ScopedVars, |
||||
} from '@grafana/data'; |
||||
|
||||
import { VariableIdentifier } from '../state/types'; |
||||
import { getVariable } from '../state/selectors'; |
||||
import { QueryVariableModel, VariableRefresh } from '../types'; |
||||
import { StoreState, ThunkDispatch } from '../../../types'; |
||||
import { dispatch, getState } from '../../../store/store'; |
||||
import { getTemplatedRegex } from '../utils'; |
||||
import { v4 as uuidv4 } from 'uuid'; |
||||
import { getTimeSrv } from '../../dashboard/services/TimeSrv'; |
||||
import { QueryRunners } from './queryRunners'; |
||||
import { runRequest } from '../../dashboard/state/runRequest'; |
||||
import { |
||||
runUpdateTagsRequest, |
||||
toMetricFindValues, |
||||
updateOptionsState, |
||||
updateTagsState, |
||||
validateVariableSelection, |
||||
} from './operators'; |
||||
|
||||
interface UpdateOptionsArgs { |
||||
identifier: VariableIdentifier; |
||||
datasource: DataSourceApi; |
||||
searchFilter?: string; |
||||
} |
||||
|
||||
export interface UpdateOptionsResults { |
||||
state: LoadingState; |
||||
identifier: VariableIdentifier; |
||||
error?: any; |
||||
cancelled?: boolean; |
||||
} |
||||
|
||||
interface VariableQueryRunnerArgs { |
||||
dispatch: ThunkDispatch; |
||||
getState: () => StoreState; |
||||
getVariable: typeof getVariable; |
||||
getTemplatedRegex: typeof getTemplatedRegex; |
||||
getTimeSrv: typeof getTimeSrv; |
||||
queryRunners: QueryRunners; |
||||
runRequest: typeof runRequest; |
||||
} |
||||
|
||||
export class VariableQueryRunner { |
||||
private readonly updateOptionsRequests: Subject<UpdateOptionsArgs>; |
||||
private readonly updateOptionsResults: Subject<UpdateOptionsResults>; |
||||
private readonly cancelRequests: Subject<{ identifier: VariableIdentifier }>; |
||||
private readonly subscription: Unsubscribable; |
||||
|
||||
constructor( |
||||
private dependencies: VariableQueryRunnerArgs = { |
||||
dispatch, |
||||
getState, |
||||
getVariable, |
||||
getTemplatedRegex, |
||||
getTimeSrv, |
||||
queryRunners: new QueryRunners(), |
||||
runRequest, |
||||
} |
||||
) { |
||||
this.updateOptionsRequests = new Subject<UpdateOptionsArgs>(); |
||||
this.updateOptionsResults = new Subject<UpdateOptionsResults>(); |
||||
this.cancelRequests = new Subject<{ identifier: VariableIdentifier }>(); |
||||
this.onNewRequest = this.onNewRequest.bind(this); |
||||
this.subscription = this.updateOptionsRequests.subscribe(this.onNewRequest); |
||||
} |
||||
|
||||
queueRequest(args: UpdateOptionsArgs): void { |
||||
this.updateOptionsRequests.next(args); |
||||
} |
||||
|
||||
getResponse(identifier: VariableIdentifier): Observable<UpdateOptionsResults> { |
||||
return this.updateOptionsResults.asObservable().pipe(filter(result => result.identifier === identifier)); |
||||
} |
||||
|
||||
cancelRequest(identifier: VariableIdentifier): void { |
||||
this.cancelRequests.next({ identifier }); |
||||
} |
||||
|
||||
destroy(): void { |
||||
this.subscription.unsubscribe(); |
||||
} |
||||
|
||||
private onNewRequest(args: UpdateOptionsArgs): void { |
||||
const { datasource, identifier, searchFilter } = args; |
||||
try { |
||||
const { |
||||
dispatch, |
||||
runRequest, |
||||
getTemplatedRegex: getTemplatedRegexFunc, |
||||
getVariable, |
||||
queryRunners, |
||||
getTimeSrv, |
||||
getState, |
||||
} = this.dependencies; |
||||
|
||||
const beforeUid = getState().templating.transaction.uid; |
||||
|
||||
this.updateOptionsResults.next({ identifier, state: LoadingState.Loading }); |
||||
|
||||
const variable = getVariable<QueryVariableModel>(identifier.id, getState()); |
||||
const timeSrv = getTimeSrv(); |
||||
const runnerArgs = { variable, datasource, searchFilter, timeSrv, runRequest }; |
||||
const runner = queryRunners.getRunnerForDatasource(datasource); |
||||
const target = runner.getTarget({ datasource, variable }); |
||||
const request = this.getRequest(variable, args, target); |
||||
|
||||
runner |
||||
.runRequest(runnerArgs, request) |
||||
.pipe( |
||||
filter(() => { |
||||
// lets check if we started another batch during the execution of the observable. If so we just want to abort the rest.
|
||||
const afterUid = getState().templating.transaction.uid; |
||||
return beforeUid === afterUid; |
||||
}), |
||||
first(data => data.state === LoadingState.Done || data.state === LoadingState.Error), |
||||
mergeMap(data => { |
||||
if (data.state === LoadingState.Error) { |
||||
return throwError(data.error); |
||||
} |
||||
|
||||
return of(data); |
||||
}), |
||||
toMetricFindValues(), |
||||
updateOptionsState({ variable, dispatch, getTemplatedRegexFunc }), |
||||
runUpdateTagsRequest({ variable, datasource, searchFilter }), |
||||
updateTagsState({ variable, dispatch }), |
||||
validateVariableSelection({ variable, dispatch, searchFilter }), |
||||
takeUntil( |
||||
merge(this.updateOptionsRequests, this.cancelRequests).pipe( |
||||
filter(args => { |
||||
let cancelRequest = false; |
||||
|
||||
if (args.identifier.id === identifier.id) { |
||||
cancelRequest = true; |
||||
this.updateOptionsResults.next({ identifier, state: LoadingState.Loading, cancelled: cancelRequest }); |
||||
} |
||||
|
||||
return cancelRequest; |
||||
}) |
||||
) |
||||
), |
||||
catchError(error => { |
||||
if (error.cancelled) { |
||||
return of({}); |
||||
} |
||||
|
||||
this.updateOptionsResults.next({ identifier, state: LoadingState.Error, error }); |
||||
return throwError(error); |
||||
}), |
||||
finalize(() => { |
||||
this.updateOptionsResults.next({ identifier, state: LoadingState.Done }); |
||||
}) |
||||
) |
||||
.subscribe(); |
||||
} catch (error) { |
||||
this.updateOptionsResults.next({ identifier, state: LoadingState.Error, error }); |
||||
} |
||||
} |
||||
|
||||
private getRequest(variable: QueryVariableModel, args: UpdateOptionsArgs, target: DataQuery) { |
||||
const { searchFilter } = args; |
||||
const variableAsVars = { variable: { text: variable.current.text, value: variable.current.value } }; |
||||
const searchFilterScope = { searchFilter: { text: searchFilter, value: searchFilter } }; |
||||
const searchFilterAsVars = searchFilter ? searchFilterScope : {}; |
||||
const scopedVars = { ...searchFilterAsVars, ...variableAsVars } as ScopedVars; |
||||
const range = |
||||
variable.refresh === VariableRefresh.onTimeRangeChanged |
||||
? this.dependencies.getTimeSrv().timeRange() |
||||
: DefaultTimeRange; |
||||
|
||||
const request: DataQueryRequest = { |
||||
app: CoreApp.Dashboard, |
||||
requestId: uuidv4(), |
||||
timezone: '', |
||||
range, |
||||
interval: '', |
||||
intervalMs: 0, |
||||
targets: [target], |
||||
scopedVars, |
||||
startTime: Date.now(), |
||||
}; |
||||
|
||||
return request; |
||||
} |
||||
} |
||||
|
||||
let singleton: VariableQueryRunner; |
||||
|
||||
export function setVariableQueryRunner(runner: VariableQueryRunner): void { |
||||
singleton = runner; |
||||
} |
||||
|
||||
export function getVariableQueryRunner(): VariableQueryRunner { |
||||
return singleton; |
||||
} |
||||
@ -0,0 +1,332 @@ |
||||
import { of } from 'rxjs'; |
||||
import { queryBuilder } from '../shared/testing/builders'; |
||||
import { FieldType, observableTester, toDataFrame } from '@grafana/data'; |
||||
import { initialQueryVariableModelState, updateVariableOptions, updateVariableTags } from './reducer'; |
||||
import { toVariablePayload } from '../state/types'; |
||||
import { VariableRefresh } from '../types'; |
||||
import { |
||||
areMetricFindValues, |
||||
runUpdateTagsRequest, |
||||
toMetricFindValues, |
||||
updateOptionsState, |
||||
updateTagsState, |
||||
validateVariableSelection, |
||||
} from './operators'; |
||||
|
||||
describe('operators', () => { |
||||
describe('validateVariableSelection', () => { |
||||
describe('when called', () => { |
||||
it('then the correct observable should be created', done => { |
||||
const variable = queryBuilder() |
||||
.withId('query') |
||||
.build(); |
||||
const dispatch = jest.fn().mockResolvedValue({}); |
||||
const observable = of(undefined).pipe( |
||||
validateVariableSelection({ variable, dispatch, searchFilter: 'A search filter' }) |
||||
); |
||||
|
||||
observableTester().subscribeAndExpectOnNext({ |
||||
observable, |
||||
expect: value => { |
||||
expect(value).toEqual({}); |
||||
expect(dispatch).toHaveBeenCalledTimes(1); |
||||
}, |
||||
done, |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('updateTagsState', () => { |
||||
describe('when called with a variable that uses Tags', () => { |
||||
it('then the correct observable should be created', done => { |
||||
const variable = queryBuilder() |
||||
.withId('query') |
||||
.withTags(true) |
||||
.build(); |
||||
const dispatch = jest.fn().mockResolvedValue({}); |
||||
const observable = of([{ text: 'A text' }]).pipe(updateTagsState({ variable, dispatch })); |
||||
|
||||
observableTester().subscribeAndExpectOnNext({ |
||||
observable, |
||||
expect: value => { |
||||
expect(value).toEqual(undefined); |
||||
expect(dispatch).toHaveBeenCalledTimes(1); |
||||
expect(dispatch).toHaveBeenCalledWith( |
||||
updateVariableTags(toVariablePayload(variable, [{ text: 'A text' }])) |
||||
); |
||||
}, |
||||
done, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a variable that does not use Tags', () => { |
||||
it('then the correct observable should be created', done => { |
||||
const variable = queryBuilder() |
||||
.withId('query') |
||||
.withTags(false) |
||||
.build(); |
||||
const dispatch = jest.fn().mockResolvedValue({}); |
||||
const observable = of([{ text: 'A text' }]).pipe(updateTagsState({ variable, dispatch })); |
||||
|
||||
observableTester().subscribeAndExpectOnNext({ |
||||
observable, |
||||
expect: value => { |
||||
expect(value).toEqual(undefined); |
||||
expect(dispatch).not.toHaveBeenCalled(); |
||||
}, |
||||
done, |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('runUpdateTagsRequest', () => { |
||||
describe('when called with a datasource with metricFindQuery and variable that uses Tags and refreshes on time range changes', () => { |
||||
it('then the correct observable should be created', done => { |
||||
const variable = queryBuilder() |
||||
.withId('query') |
||||
.withTags(true) |
||||
.withTagsQuery('A tags query') |
||||
.withRefresh(VariableRefresh.onTimeRangeChanged) |
||||
.build(); |
||||
const timeSrv: any = { |
||||
timeRange: jest.fn(), |
||||
}; |
||||
const datasource: any = { metricFindQuery: jest.fn().mockResolvedValue([{ text: 'A text' }]) }; |
||||
const searchFilter = 'A search filter'; |
||||
const observable = of(undefined).pipe(runUpdateTagsRequest({ variable, datasource, searchFilter }, timeSrv)); |
||||
|
||||
observableTester().subscribeAndExpectOnNext({ |
||||
observable, |
||||
expect: value => { |
||||
const { index, global, ...rest } = initialQueryVariableModelState; |
||||
expect(value).toEqual([{ text: 'A text' }]); |
||||
expect(timeSrv.timeRange).toHaveBeenCalledTimes(1); |
||||
expect(datasource.metricFindQuery).toHaveBeenCalledTimes(1); |
||||
expect(datasource.metricFindQuery).toHaveBeenCalledWith('A tags query', { |
||||
range: undefined, |
||||
searchFilter: 'A search filter', |
||||
variable: { |
||||
...rest, |
||||
id: 'query', |
||||
name: 'query', |
||||
useTags: true, |
||||
tagsQuery: 'A tags query', |
||||
refresh: VariableRefresh.onTimeRangeChanged, |
||||
}, |
||||
}); |
||||
}, |
||||
done, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a datasource without metricFindQuery and variable that uses Tags and refreshes on time range changes', () => { |
||||
it('then the correct observable should be created', done => { |
||||
const variable = queryBuilder() |
||||
.withId('query') |
||||
.withTags(true) |
||||
.withTagsQuery('A tags query') |
||||
.withRefresh(VariableRefresh.onTimeRangeChanged) |
||||
.build(); |
||||
const timeSrv: any = { |
||||
timeRange: jest.fn(), |
||||
}; |
||||
const datasource: any = {}; |
||||
const searchFilter = 'A search filter'; |
||||
const observable = of(undefined).pipe(runUpdateTagsRequest({ variable, datasource, searchFilter }, timeSrv)); |
||||
|
||||
observableTester().subscribeAndExpectOnNext({ |
||||
observable, |
||||
expect: value => { |
||||
expect(value).toEqual([]); |
||||
expect(timeSrv.timeRange).not.toHaveBeenCalled(); |
||||
}, |
||||
done, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a datasource with metricFindQuery and variable that does not use Tags but refreshes on time range changes', () => { |
||||
it('then the correct observable should be created', done => { |
||||
const variable = queryBuilder() |
||||
.withId('query') |
||||
.withTags(false) |
||||
.withRefresh(VariableRefresh.onTimeRangeChanged) |
||||
.build(); |
||||
const timeSrv: any = { |
||||
timeRange: jest.fn(), |
||||
}; |
||||
const datasource: any = { metricFindQuery: jest.fn().mockResolvedValue([{ text: 'A text' }]) }; |
||||
const searchFilter = 'A search filter'; |
||||
const observable = of(undefined).pipe(runUpdateTagsRequest({ variable, datasource, searchFilter }, timeSrv)); |
||||
|
||||
observableTester().subscribeAndExpectOnNext({ |
||||
observable, |
||||
expect: value => { |
||||
expect(value).toEqual([]); |
||||
expect(timeSrv.timeRange).not.toHaveBeenCalled(); |
||||
expect(datasource.metricFindQuery).not.toHaveBeenCalled(); |
||||
}, |
||||
done, |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('updateOptionsState', () => { |
||||
describe('when called', () => { |
||||
it('then the correct observable should be created', done => { |
||||
const variable = queryBuilder() |
||||
.withId('query') |
||||
.build(); |
||||
const dispatch = jest.fn(); |
||||
const getTemplatedRegexFunc = jest.fn().mockReturnValue('getTemplatedRegexFunc result'); |
||||
|
||||
const observable = of([{ text: 'A' }]).pipe(updateOptionsState({ variable, dispatch, getTemplatedRegexFunc })); |
||||
|
||||
observableTester().subscribeAndExpectOnNext({ |
||||
observable, |
||||
expect: value => { |
||||
expect(value).toEqual(undefined); |
||||
expect(getTemplatedRegexFunc).toHaveBeenCalledTimes(1); |
||||
expect(dispatch).toHaveBeenCalledTimes(1); |
||||
expect(dispatch).toHaveBeenCalledWith( |
||||
updateVariableOptions({ |
||||
id: 'query', |
||||
type: 'query', |
||||
data: { results: [{ text: 'A' }], templatedRegex: 'getTemplatedRegexFunc result' }, |
||||
}) |
||||
); |
||||
}, |
||||
done, |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('toMetricFindValues', () => { |
||||
const frameWithTextField = toDataFrame({ |
||||
fields: [{ name: 'text', type: FieldType.string, values: ['A', 'B', 'C'] }], |
||||
}); |
||||
const frameWithValueField = toDataFrame({ |
||||
fields: [{ name: 'value', type: FieldType.string, values: ['A', 'B', 'C'] }], |
||||
}); |
||||
const frameWithTextAndValueField = toDataFrame({ |
||||
fields: [ |
||||
{ name: 'text', type: FieldType.string, values: ['TA', 'TB', 'TC'] }, |
||||
{ name: 'value', type: FieldType.string, values: ['VA', 'VB', 'VC'] }, |
||||
], |
||||
}); |
||||
const frameWithAStringField = toDataFrame({ |
||||
fields: [{ name: 'label', type: FieldType.string, values: ['A', 'B', 'C'] }], |
||||
}); |
||||
const frameWithExpandableField = toDataFrame({ |
||||
fields: [ |
||||
{ name: 'label', type: FieldType.string, values: ['A', 'B', 'C'] }, |
||||
{ name: 'expandable', type: FieldType.boolean, values: [true, false, true] }, |
||||
], |
||||
}); |
||||
|
||||
// it.each wouldn't work here as we need the done callback
|
||||
[ |
||||
{ series: null, expected: [] }, |
||||
{ series: undefined, expected: [] }, |
||||
{ series: [], expected: [] }, |
||||
{ series: [{ text: '' }], expected: [{ text: '' }] }, |
||||
{ series: [{ value: '' }], expected: [{ value: '' }] }, |
||||
{ |
||||
series: [frameWithTextField], |
||||
expected: [ |
||||
{ text: 'A', value: 'A' }, |
||||
{ text: 'B', value: 'B' }, |
||||
{ text: 'C', value: 'C' }, |
||||
], |
||||
}, |
||||
{ |
||||
series: [frameWithValueField], |
||||
expected: [ |
||||
{ text: 'A', value: 'A' }, |
||||
{ text: 'B', value: 'B' }, |
||||
{ text: 'C', value: 'C' }, |
||||
], |
||||
}, |
||||
{ |
||||
series: [frameWithTextAndValueField], |
||||
expected: [ |
||||
{ text: 'TA', value: 'VA' }, |
||||
{ text: 'TB', value: 'VB' }, |
||||
{ text: 'TC', value: 'VC' }, |
||||
], |
||||
}, |
||||
{ |
||||
series: [frameWithAStringField], |
||||
expected: [ |
||||
{ text: 'A', value: 'A' }, |
||||
{ text: 'B', value: 'B' }, |
||||
{ text: 'C', value: 'C' }, |
||||
], |
||||
}, |
||||
{ |
||||
series: [frameWithExpandableField], |
||||
expected: [ |
||||
{ text: 'A', value: 'A', expandable: true }, |
||||
{ text: 'B', value: 'B', expandable: false }, |
||||
{ text: 'C', value: 'C', expandable: true }, |
||||
], |
||||
}, |
||||
].map(scenario => { |
||||
it(`when called with series:${JSON.stringify(scenario.series, null, 0)}`, done => { |
||||
const { series, expected } = scenario; |
||||
const panelData: any = { series }; |
||||
const observable = of(panelData).pipe(toMetricFindValues()); |
||||
|
||||
observableTester().subscribeAndExpectOnNext({ |
||||
observable, |
||||
expect: value => { |
||||
expect(value).toEqual(expected); |
||||
}, |
||||
done, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called without metric find values and string fields', () => { |
||||
it('then the observable throws', done => { |
||||
const frameWithTimeField = toDataFrame({ |
||||
fields: [{ name: 'time', type: FieldType.time, values: [1, 2, 3] }], |
||||
}); |
||||
|
||||
const panelData: any = { series: [frameWithTimeField] }; |
||||
const observable = of(panelData).pipe(toMetricFindValues()); |
||||
|
||||
observableTester().subscribeAndExpectOnError({ |
||||
observable, |
||||
expect: value => { |
||||
expect(value).toEqual(new Error("Couldn't find any field of type string in the results.")); |
||||
}, |
||||
done, |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('areMetricFindValues', () => { |
||||
it.each` |
||||
values | expected |
||||
${null} | ${false} |
||||
${undefined} | ${false} |
||||
${[]} | ${true} |
||||
${[{ text: '' }]} | ${true} |
||||
${[{ Text: '' }]} | ${true} |
||||
${[{ value: '' }]} | ${true} |
||||
${[{ Value: '' }]} | ${true} |
||||
${[{ text: '', value: '' }]} | ${true} |
||||
${[{ Text: '', Value: '' }]} | ${true} |
||||
`('when called with values:$values', ({ values, expected }) => {
|
||||
expect(areMetricFindValues(values)).toBe(expected); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,187 @@ |
||||
import { from, of, OperatorFunction } from 'rxjs'; |
||||
import { map, mergeMap } from 'rxjs/operators'; |
||||
|
||||
import { QueryVariableModel } from '../types'; |
||||
import { ThunkDispatch } from '../../../types'; |
||||
import { toVariableIdentifier, toVariablePayload } from '../state/types'; |
||||
import { validateVariableSelectionState } from '../state/actions'; |
||||
import { DataSourceApi, FieldType, getFieldDisplayName, MetricFindValue, PanelData } from '@grafana/data'; |
||||
import { updateVariableOptions, updateVariableTags } from './reducer'; |
||||
import { getTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv'; |
||||
import { getLegacyQueryOptions, getTemplatedRegex } from '../utils'; |
||||
|
||||
const metricFindValueProps = ['text', 'Text', 'value', 'Value']; |
||||
|
||||
export function toMetricFindValues(): OperatorFunction<PanelData, MetricFindValue[]> { |
||||
return source => |
||||
source.pipe( |
||||
map(panelData => { |
||||
const frames = panelData.series; |
||||
if (!frames || !frames.length) { |
||||
return []; |
||||
} |
||||
|
||||
if (areMetricFindValues(frames)) { |
||||
return frames; |
||||
} |
||||
|
||||
const metrics: MetricFindValue[] = []; |
||||
|
||||
let valueIndex = -1; |
||||
let textIndex = -1; |
||||
let stringIndex = -1; |
||||
let expandableIndex = -1; |
||||
|
||||
for (const frame of frames) { |
||||
for (let index = 0; index < frame.fields.length; index++) { |
||||
const field = frame.fields[index]; |
||||
const fieldName = getFieldDisplayName(field, frame, frames).toLowerCase(); |
||||
|
||||
if (field.type === FieldType.string && stringIndex === -1) { |
||||
stringIndex = index; |
||||
} |
||||
|
||||
if (fieldName === 'text' && field.type === FieldType.string && textIndex === -1) { |
||||
textIndex = index; |
||||
} |
||||
|
||||
if (fieldName === 'value' && field.type === FieldType.string && valueIndex === -1) { |
||||
valueIndex = index; |
||||
} |
||||
|
||||
if ( |
||||
fieldName === 'expandable' && |
||||
(field.type === FieldType.boolean || field.type === FieldType.number) && |
||||
expandableIndex === -1 |
||||
) { |
||||
expandableIndex = index; |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (stringIndex === -1) { |
||||
throw new Error("Couldn't find any field of type string in the results."); |
||||
} |
||||
|
||||
for (const frame of frames) { |
||||
for (let index = 0; index < frame.length; index++) { |
||||
const expandable = expandableIndex !== -1 ? frame.fields[expandableIndex].values.get(index) : undefined; |
||||
const string = frame.fields[stringIndex].values.get(index); |
||||
const text = textIndex !== -1 ? frame.fields[textIndex].values.get(index) : null; |
||||
const value = valueIndex !== -1 ? frame.fields[valueIndex].values.get(index) : null; |
||||
|
||||
if (valueIndex === -1 && textIndex === -1) { |
||||
metrics.push({ text: string, value: string, expandable }); |
||||
continue; |
||||
} |
||||
|
||||
if (valueIndex === -1 && textIndex !== -1) { |
||||
metrics.push({ text, value: text, expandable }); |
||||
continue; |
||||
} |
||||
|
||||
if (valueIndex !== -1 && textIndex === -1) { |
||||
metrics.push({ text: value, value, expandable }); |
||||
continue; |
||||
} |
||||
|
||||
metrics.push({ text, value, expandable }); |
||||
} |
||||
} |
||||
|
||||
return metrics; |
||||
}) |
||||
); |
||||
} |
||||
|
||||
export function updateOptionsState(args: { |
||||
variable: QueryVariableModel; |
||||
dispatch: ThunkDispatch; |
||||
getTemplatedRegexFunc: typeof getTemplatedRegex; |
||||
}): OperatorFunction<MetricFindValue[], void> { |
||||
return source => |
||||
source.pipe( |
||||
map(results => { |
||||
const { variable, dispatch, getTemplatedRegexFunc } = args; |
||||
const templatedRegex = getTemplatedRegexFunc(variable); |
||||
const payload = toVariablePayload(variable, { results, templatedRegex }); |
||||
dispatch(updateVariableOptions(payload)); |
||||
}) |
||||
); |
||||
} |
||||
|
||||
export function runUpdateTagsRequest( |
||||
args: { |
||||
variable: QueryVariableModel; |
||||
datasource: DataSourceApi; |
||||
searchFilter?: string; |
||||
}, |
||||
timeSrv: TimeSrv = getTimeSrv() |
||||
): OperatorFunction<void, MetricFindValue[]> { |
||||
return source => |
||||
source.pipe( |
||||
mergeMap(() => { |
||||
const { datasource, searchFilter, variable } = args; |
||||
|
||||
if (variable.useTags && datasource.metricFindQuery) { |
||||
return from( |
||||
datasource.metricFindQuery(variable.tagsQuery, getLegacyQueryOptions(variable, searchFilter, timeSrv)) |
||||
); |
||||
} |
||||
|
||||
return of([]); |
||||
}) |
||||
); |
||||
} |
||||
|
||||
export function updateTagsState(args: { |
||||
variable: QueryVariableModel; |
||||
dispatch: ThunkDispatch; |
||||
}): OperatorFunction<MetricFindValue[], void> { |
||||
return source => |
||||
source.pipe( |
||||
map(tagResults => { |
||||
const { dispatch, variable } = args; |
||||
|
||||
if (variable.useTags) { |
||||
dispatch(updateVariableTags(toVariablePayload(variable, tagResults))); |
||||
} |
||||
}) |
||||
); |
||||
} |
||||
|
||||
export function validateVariableSelection(args: { |
||||
variable: QueryVariableModel; |
||||
dispatch: ThunkDispatch; |
||||
searchFilter?: string; |
||||
}): OperatorFunction<void, void> { |
||||
return source => |
||||
source.pipe( |
||||
mergeMap(() => { |
||||
const { dispatch, variable, searchFilter } = args; |
||||
|
||||
// If we are searching options there is no need to validate selection state
|
||||
// This condition was added to as validateVariableSelectionState will update the current value of the variable
|
||||
// So after search and selection the current value is already update so no setValue, refresh & url update is performed
|
||||
// The if statement below fixes https://github.com/grafana/grafana/issues/25671
|
||||
if (!searchFilter) { |
||||
return from(dispatch(validateVariableSelectionState(toVariableIdentifier(variable)))); |
||||
} |
||||
|
||||
return of<void>(); |
||||
}) |
||||
); |
||||
} |
||||
|
||||
export function areMetricFindValues(data: any[]): data is MetricFindValue[] { |
||||
if (!data) { |
||||
return false; |
||||
} |
||||
|
||||
if (!data.length) { |
||||
return true; |
||||
} |
||||
|
||||
const firstValue: any = data[0]; |
||||
return metricFindValueProps.some(prop => firstValue.hasOwnProperty(prop) && typeof firstValue[prop] === 'string'); |
||||
} |
||||
@ -0,0 +1,316 @@ |
||||
import { QueryRunners } from './queryRunners'; |
||||
import { DefaultTimeRange, observableTester, VariableSupportType } from '@grafana/data'; |
||||
import { VariableRefresh } from '../types'; |
||||
import { of } from 'rxjs'; |
||||
|
||||
describe('QueryRunners', () => { |
||||
describe('when using a legacy data source', () => { |
||||
const getLegacyTestContext = (variable?: any) => { |
||||
variable = variable ?? { query: 'A query' }; |
||||
const timeSrv = { |
||||
timeRange: jest.fn().mockReturnValue(DefaultTimeRange), |
||||
}; |
||||
const datasource: any = { metricFindQuery: jest.fn().mockResolvedValue([{ text: 'A', value: 'A' }]) }; |
||||
const runner = new QueryRunners().getRunnerForDatasource(datasource); |
||||
const runRequest = jest.fn().mockReturnValue(of({})); |
||||
const runnerArgs: any = { datasource, variable, searchFilter: 'A searchFilter', timeSrv, runRequest }; |
||||
const request: any = {}; |
||||
|
||||
return { timeSrv, datasource, runner, variable, runnerArgs, request }; |
||||
}; |
||||
|
||||
describe('and calling getRunnerForDatasource', () => { |
||||
it('then it should return LegacyQueryRunner', () => { |
||||
const { runner } = getLegacyTestContext(); |
||||
expect(runner!.type).toEqual(VariableSupportType.Legacy); |
||||
}); |
||||
}); |
||||
|
||||
describe('and calling getTarget', () => { |
||||
it('then it should return correct target', () => { |
||||
const { runner, datasource, variable } = getLegacyTestContext(); |
||||
const target = runner.getTarget({ datasource, variable }); |
||||
expect(target).toEqual('A query'); |
||||
}); |
||||
}); |
||||
|
||||
describe('and calling runRequest with a variable that refreshes when time range changes', () => { |
||||
const { datasource, runner, runnerArgs, request, timeSrv } = getLegacyTestContext({ |
||||
query: 'A query', |
||||
refresh: VariableRefresh.onTimeRangeChanged, |
||||
}); |
||||
const observable = runner.runRequest(runnerArgs, request); |
||||
|
||||
it('then it should return correct observable', done => { |
||||
observableTester().subscribeAndExpectOnNext({ |
||||
observable, |
||||
expect: values => { |
||||
expect(values).toEqual({ |
||||
series: [{ text: 'A', value: 'A' }], |
||||
state: 'Done', |
||||
timeRange: { from: {}, raw: { from: '6h', to: 'now' }, to: {} }, |
||||
}); |
||||
}, |
||||
done, |
||||
}); |
||||
}); |
||||
|
||||
it('and it should call timeSrv.timeRange()', () => { |
||||
expect(timeSrv.timeRange).toHaveBeenCalledTimes(1); |
||||
}); |
||||
|
||||
it('and it should call metricFindQuery with correct options', () => { |
||||
expect(datasource.metricFindQuery).toHaveBeenCalledTimes(1); |
||||
expect(datasource.metricFindQuery).toHaveBeenCalledWith('A query', { |
||||
range: { |
||||
from: {}, |
||||
raw: { |
||||
from: '6h', |
||||
to: 'now', |
||||
}, |
||||
to: {}, |
||||
}, |
||||
searchFilter: 'A searchFilter', |
||||
variable: { |
||||
query: 'A query', |
||||
refresh: VariableRefresh.onTimeRangeChanged, |
||||
}, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('and calling runRequest with a variable that does not refresh when time range changes', () => { |
||||
const { datasource, runner, runnerArgs, request, timeSrv } = getLegacyTestContext({ |
||||
query: 'A query', |
||||
refresh: VariableRefresh.never, |
||||
}); |
||||
const observable = runner.runRequest(runnerArgs, request); |
||||
|
||||
it('then it should return correct observable', done => { |
||||
observableTester().subscribeAndExpectOnNext({ |
||||
observable, |
||||
expect: values => { |
||||
expect(values).toEqual({ |
||||
series: [{ text: 'A', value: 'A' }], |
||||
state: 'Done', |
||||
timeRange: { from: {}, raw: { from: '6h', to: 'now' }, to: {} }, |
||||
}); |
||||
}, |
||||
done, |
||||
}); |
||||
}); |
||||
|
||||
it('and it should not call timeSrv.timeRange()', () => { |
||||
expect(timeSrv.timeRange).not.toHaveBeenCalled(); |
||||
}); |
||||
|
||||
it('and it should call metricFindQuery with correct options', () => { |
||||
expect(datasource.metricFindQuery).toHaveBeenCalledTimes(1); |
||||
expect(datasource.metricFindQuery).toHaveBeenCalledWith('A query', { |
||||
range: undefined, |
||||
searchFilter: 'A searchFilter', |
||||
variable: { |
||||
query: 'A query', |
||||
refresh: VariableRefresh.never, |
||||
}, |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when using a data source with standard variable support', () => { |
||||
const getStandardTestContext = (datasource?: any) => { |
||||
const variable: any = { query: { refId: 'A', query: 'A query' } }; |
||||
const timeSrv = {}; |
||||
datasource = datasource ?? { |
||||
variables: { |
||||
getType: () => VariableSupportType.Standard, |
||||
toDataQuery: (query: any) => ({ ...query, extra: 'extra' }), |
||||
}, |
||||
}; |
||||
const runner = new QueryRunners().getRunnerForDatasource(datasource); |
||||
const runRequest = jest.fn().mockReturnValue(of({})); |
||||
const runnerArgs: any = { datasource, variable, searchFilter: 'A searchFilter', timeSrv, runRequest }; |
||||
const request: any = {}; |
||||
|
||||
return { timeSrv, datasource, runner, variable, runnerArgs, request, runRequest }; |
||||
}; |
||||
|
||||
describe('and calling getRunnerForDatasource', () => { |
||||
it('then it should return StandardQueryRunner', () => { |
||||
const { runner } = getStandardTestContext(); |
||||
expect(runner!.type).toEqual(VariableSupportType.Standard); |
||||
}); |
||||
}); |
||||
|
||||
describe('and calling getTarget', () => { |
||||
it('then it should return correct target', () => { |
||||
const { runner, variable, datasource } = getStandardTestContext(); |
||||
const target = runner.getTarget({ datasource, variable }); |
||||
expect(target).toEqual({ refId: 'A', query: 'A query', extra: 'extra' }); |
||||
}); |
||||
}); |
||||
|
||||
describe('and calling runRequest with a datasource that uses a custom query', () => { |
||||
const { runner, request, runnerArgs, runRequest, datasource } = getStandardTestContext({ |
||||
variables: { |
||||
getType: () => VariableSupportType.Standard, |
||||
toDataQuery: () => undefined, |
||||
query: () => undefined, |
||||
}, |
||||
}); |
||||
const observable = runner.runRequest(runnerArgs, request); |
||||
|
||||
it('then it should return correct observable', done => { |
||||
observableTester().subscribeAndExpectOnNext({ |
||||
observable, |
||||
expect: value => { |
||||
expect(value).toEqual({}); |
||||
}, |
||||
done, |
||||
}); |
||||
}); |
||||
|
||||
it('then it should call runRequest with correct args', () => { |
||||
expect(runRequest).toHaveBeenCalledTimes(1); |
||||
expect(runRequest).toHaveBeenCalledWith(datasource, {}, datasource.variables.query); |
||||
}); |
||||
}); |
||||
|
||||
describe('and calling runRequest with a datasource that has no custom query', () => { |
||||
const { runner, request, runnerArgs, runRequest, datasource } = getStandardTestContext({ |
||||
variables: { getType: () => VariableSupportType.Standard, toDataQuery: () => undefined }, |
||||
}); |
||||
const observable = runner.runRequest(runnerArgs, request); |
||||
|
||||
it('then it should return correct observable', done => { |
||||
observableTester().subscribeAndExpectOnNext({ |
||||
observable, |
||||
expect: value => { |
||||
expect(value).toEqual({}); |
||||
}, |
||||
done, |
||||
}); |
||||
}); |
||||
|
||||
it('then it should call runRequest with correct args', () => { |
||||
expect(runRequest).toHaveBeenCalledTimes(1); |
||||
expect(runRequest).toHaveBeenCalledWith(datasource, {}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when using a data source with custom variable support', () => { |
||||
const getCustomTestContext = () => { |
||||
const variable: any = { query: { refId: 'A', query: 'A query' } }; |
||||
const timeSrv = {}; |
||||
const datasource: any = { |
||||
variables: { getType: () => VariableSupportType.Custom, query: () => undefined, editor: {} }, |
||||
}; |
||||
const runner = new QueryRunners().getRunnerForDatasource(datasource); |
||||
const runRequest = jest.fn().mockReturnValue(of({})); |
||||
const runnerArgs: any = { datasource, variable, searchFilter: 'A searchFilter', timeSrv, runRequest }; |
||||
const request: any = {}; |
||||
|
||||
return { timeSrv, datasource, runner, variable, runnerArgs, request, runRequest }; |
||||
}; |
||||
|
||||
describe('and calling getRunnerForDatasource', () => { |
||||
it('then it should return CustomQueryRunner', () => { |
||||
const { runner } = getCustomTestContext(); |
||||
expect(runner!.type).toEqual(VariableSupportType.Custom); |
||||
}); |
||||
}); |
||||
|
||||
describe('and calling getTarget', () => { |
||||
it('then it should return correct target', () => { |
||||
const { runner, variable, datasource } = getCustomTestContext(); |
||||
const target = runner.getTarget({ datasource, variable }); |
||||
expect(target).toEqual({ refId: 'A', query: 'A query' }); |
||||
}); |
||||
}); |
||||
|
||||
describe('and calling runRequest', () => { |
||||
const { runner, request, runnerArgs, runRequest, datasource } = getCustomTestContext(); |
||||
const observable = runner.runRequest(runnerArgs, request); |
||||
|
||||
it('then it should return correct observable', done => { |
||||
observableTester().subscribeAndExpectOnNext({ |
||||
observable, |
||||
expect: value => { |
||||
expect(value).toEqual({}); |
||||
}, |
||||
done, |
||||
}); |
||||
}); |
||||
|
||||
it('then it should call runRequest with correct args', () => { |
||||
expect(runRequest).toHaveBeenCalledTimes(1); |
||||
expect(runRequest).toHaveBeenCalledWith(datasource, {}, datasource.variables.query); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when using a data source with datasource variable support', () => { |
||||
const getDatasourceTestContext = () => { |
||||
const variable: any = { query: { refId: 'A', query: 'A query' } }; |
||||
const timeSrv = {}; |
||||
const datasource: any = { |
||||
variables: { getType: () => VariableSupportType.Datasource }, |
||||
}; |
||||
const runner = new QueryRunners().getRunnerForDatasource(datasource); |
||||
const runRequest = jest.fn().mockReturnValue(of({})); |
||||
const runnerArgs: any = { datasource, variable, searchFilter: 'A searchFilter', timeSrv, runRequest }; |
||||
const request: any = {}; |
||||
|
||||
return { timeSrv, datasource, runner, variable, runnerArgs, request, runRequest }; |
||||
}; |
||||
|
||||
describe('and calling getRunnerForDatasource', () => { |
||||
it('then it should return DatasourceQueryRunner', () => { |
||||
const { runner } = getDatasourceTestContext(); |
||||
expect(runner!.type).toEqual(VariableSupportType.Datasource); |
||||
}); |
||||
}); |
||||
|
||||
describe('and calling getTarget', () => { |
||||
it('then it should return correct target', () => { |
||||
const { runner, datasource, variable } = getDatasourceTestContext(); |
||||
const target = runner.getTarget({ datasource, variable }); |
||||
expect(target).toEqual({ refId: 'A', query: 'A query' }); |
||||
}); |
||||
}); |
||||
|
||||
describe('and calling runRequest', () => { |
||||
const { runner, request, runnerArgs, runRequest, datasource } = getDatasourceTestContext(); |
||||
const observable = runner.runRequest(runnerArgs, request); |
||||
|
||||
it('then it should return correct observable', done => { |
||||
observableTester().subscribeAndExpectOnNext({ |
||||
observable, |
||||
expect: value => { |
||||
expect(value).toEqual({}); |
||||
}, |
||||
done, |
||||
}); |
||||
}); |
||||
|
||||
it('then it should call runRequest with correct args', () => { |
||||
expect(runRequest).toHaveBeenCalledTimes(1); |
||||
expect(runRequest).toHaveBeenCalledWith(datasource, {}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when using a data source with unknown variable support', () => { |
||||
describe('and calling getRunnerForDatasource', () => { |
||||
it('then it should throw', () => { |
||||
const datasource: any = { |
||||
variables: {}, |
||||
}; |
||||
|
||||
expect(() => new QueryRunners().getRunnerForDatasource(datasource)).toThrow(); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,178 @@ |
||||
import { from, Observable, of } from 'rxjs'; |
||||
import { mergeMap } from 'rxjs/operators'; |
||||
import { |
||||
DataQuery, |
||||
DataQueryRequest, |
||||
DataSourceApi, |
||||
DefaultTimeRange, |
||||
LoadingState, |
||||
PanelData, |
||||
VariableSupportType, |
||||
} from '@grafana/data'; |
||||
|
||||
import { QueryVariableModel } from '../types'; |
||||
import { |
||||
hasCustomVariableSupport, |
||||
hasDatasourceVariableSupport, |
||||
hasLegacyVariableSupport, |
||||
hasStandardVariableSupport, |
||||
} from '../guard'; |
||||
import { getLegacyQueryOptions } from '../utils'; |
||||
import { TimeSrv } from '../../dashboard/services/TimeSrv'; |
||||
|
||||
export interface RunnerArgs { |
||||
variable: QueryVariableModel; |
||||
datasource: DataSourceApi; |
||||
timeSrv: TimeSrv; |
||||
runRequest: ( |
||||
datasource: DataSourceApi, |
||||
request: DataQueryRequest, |
||||
queryFunction?: typeof datasource.query |
||||
) => Observable<PanelData>; |
||||
searchFilter?: string; |
||||
} |
||||
|
||||
type GetTargetArgs = { datasource: DataSourceApi; variable: QueryVariableModel }; |
||||
|
||||
export interface QueryRunner { |
||||
type: VariableSupportType; |
||||
canRun: (dataSource: DataSourceApi) => boolean; |
||||
getTarget: (args: GetTargetArgs) => DataQuery; |
||||
runRequest: (args: RunnerArgs, request: DataQueryRequest) => Observable<PanelData>; |
||||
} |
||||
|
||||
export class QueryRunners { |
||||
private readonly runners: QueryRunner[]; |
||||
constructor() { |
||||
this.runners = [ |
||||
new LegacyQueryRunner(), |
||||
new StandardQueryRunner(), |
||||
new CustomQueryRunner(), |
||||
new DatasourceQueryRunner(), |
||||
]; |
||||
} |
||||
|
||||
getRunnerForDatasource(datasource: DataSourceApi): QueryRunner { |
||||
const runner = this.runners.find(runner => runner.canRun(datasource)); |
||||
if (runner) { |
||||
return runner; |
||||
} |
||||
|
||||
throw new Error("Couldn't find a query runner that matches supplied arguments."); |
||||
} |
||||
} |
||||
|
||||
class LegacyQueryRunner implements QueryRunner { |
||||
type = VariableSupportType.Legacy; |
||||
|
||||
canRun(dataSource: DataSourceApi) { |
||||
return hasLegacyVariableSupport(dataSource); |
||||
} |
||||
|
||||
getTarget({ datasource, variable }: GetTargetArgs) { |
||||
if (hasLegacyVariableSupport(datasource)) { |
||||
return variable.query; |
||||
} |
||||
|
||||
throw new Error("Couldn't create a target with supplied arguments."); |
||||
} |
||||
|
||||
runRequest({ datasource, variable, searchFilter, timeSrv }: RunnerArgs, request: DataQueryRequest) { |
||||
if (!hasLegacyVariableSupport(datasource)) { |
||||
return getEmptyMetricFindValueObservable(); |
||||
} |
||||
|
||||
const queryOptions: any = getLegacyQueryOptions(variable, searchFilter, timeSrv); |
||||
|
||||
return from(datasource.metricFindQuery(variable.query, queryOptions)).pipe( |
||||
mergeMap(values => { |
||||
if (!values || !values.length) { |
||||
return getEmptyMetricFindValueObservable(); |
||||
} |
||||
|
||||
const series: any = values; |
||||
return of({ series, state: LoadingState.Done, timeRange: DefaultTimeRange }); |
||||
}) |
||||
); |
||||
} |
||||
} |
||||
|
||||
class StandardQueryRunner implements QueryRunner { |
||||
type = VariableSupportType.Standard; |
||||
|
||||
canRun(dataSource: DataSourceApi) { |
||||
return hasStandardVariableSupport(dataSource); |
||||
} |
||||
|
||||
getTarget({ datasource, variable }: GetTargetArgs) { |
||||
if (hasStandardVariableSupport(datasource)) { |
||||
return datasource.variables.toDataQuery(variable.query); |
||||
} |
||||
|
||||
throw new Error("Couldn't create a target with supplied arguments."); |
||||
} |
||||
|
||||
runRequest({ datasource, runRequest }: RunnerArgs, request: DataQueryRequest) { |
||||
if (!hasStandardVariableSupport(datasource)) { |
||||
return getEmptyMetricFindValueObservable(); |
||||
} |
||||
|
||||
if (!datasource.variables.query) { |
||||
return runRequest(datasource, request); |
||||
} |
||||
|
||||
return runRequest(datasource, request, datasource.variables.query); |
||||
} |
||||
} |
||||
|
||||
class CustomQueryRunner implements QueryRunner { |
||||
type = VariableSupportType.Custom; |
||||
|
||||
canRun(dataSource: DataSourceApi) { |
||||
return hasCustomVariableSupport(dataSource); |
||||
} |
||||
|
||||
getTarget({ datasource, variable }: GetTargetArgs) { |
||||
if (hasCustomVariableSupport(datasource)) { |
||||
return variable.query; |
||||
} |
||||
|
||||
throw new Error("Couldn't create a target with supplied arguments."); |
||||
} |
||||
|
||||
runRequest({ datasource, runRequest }: RunnerArgs, request: DataQueryRequest) { |
||||
if (!hasCustomVariableSupport(datasource)) { |
||||
return getEmptyMetricFindValueObservable(); |
||||
} |
||||
|
||||
return runRequest(datasource, request, datasource.variables.query); |
||||
} |
||||
} |
||||
|
||||
class DatasourceQueryRunner implements QueryRunner { |
||||
type = VariableSupportType.Datasource; |
||||
|
||||
canRun(dataSource: DataSourceApi) { |
||||
return hasDatasourceVariableSupport(dataSource); |
||||
} |
||||
|
||||
getTarget({ datasource, variable }: GetTargetArgs) { |
||||
if (hasDatasourceVariableSupport(datasource)) { |
||||
return variable.query; |
||||
} |
||||
|
||||
throw new Error("Couldn't create a target with supplied arguments."); |
||||
} |
||||
|
||||
runRequest({ datasource, runRequest }: RunnerArgs, request: DataQueryRequest) { |
||||
if (!hasDatasourceVariableSupport(datasource)) { |
||||
return getEmptyMetricFindValueObservable(); |
||||
} |
||||
|
||||
return runRequest(datasource, request); |
||||
} |
||||
} |
||||
|
||||
function getEmptyMetricFindValueObservable(): Observable<PanelData> { |
||||
return of({ state: LoadingState.Done, series: [], timeRange: DefaultTimeRange }); |
||||
} |
||||
@ -0,0 +1,90 @@ |
||||
import { variableQueryObserver } from './variableQueryObserver'; |
||||
import { LoadingState } from '@grafana/data'; |
||||
import { VariableIdentifier } from '../state/types'; |
||||
import { UpdateOptionsResults } from './VariableQueryRunner'; |
||||
|
||||
function getTestContext(args: { next?: UpdateOptionsResults; error?: any; complete?: boolean }) { |
||||
const { next, error, complete } = args; |
||||
const resolve = jest.fn(); |
||||
const reject = jest.fn(); |
||||
const subscription: any = { |
||||
unsubscribe: jest.fn(), |
||||
}; |
||||
const observer = variableQueryObserver(resolve, reject, subscription); |
||||
|
||||
if (next) { |
||||
observer.next(next); |
||||
} |
||||
|
||||
if (error) { |
||||
observer.error(error); |
||||
} |
||||
|
||||
if (complete) { |
||||
observer.complete(); |
||||
} |
||||
|
||||
return { resolve, reject, subscription, observer }; |
||||
} |
||||
|
||||
const identifier: VariableIdentifier = { id: 'id', type: 'query' }; |
||||
|
||||
describe('variableQueryObserver', () => { |
||||
describe('when receiving a Done state', () => { |
||||
it('then it should call unsubscribe', () => { |
||||
const { subscription } = getTestContext({ next: { state: LoadingState.Done, identifier } }); |
||||
|
||||
expect(subscription.unsubscribe).toHaveBeenCalledTimes(1); |
||||
}); |
||||
|
||||
it('then it should call resolve', () => { |
||||
const { resolve } = getTestContext({ next: { state: LoadingState.Done, identifier } }); |
||||
|
||||
expect(resolve).toHaveBeenCalledTimes(1); |
||||
}); |
||||
}); |
||||
|
||||
describe('when receiving an Error state', () => { |
||||
it('then it should call unsubscribe', () => { |
||||
const { subscription } = getTestContext({ next: { state: LoadingState.Error, identifier, error: 'An error' } }); |
||||
|
||||
expect(subscription.unsubscribe).toHaveBeenCalledTimes(1); |
||||
}); |
||||
|
||||
it('then it should call reject', () => { |
||||
const { reject } = getTestContext({ next: { state: LoadingState.Error, identifier, error: 'An error' } }); |
||||
|
||||
expect(reject).toHaveBeenCalledTimes(1); |
||||
expect(reject).toHaveBeenCalledWith('An error'); |
||||
}); |
||||
}); |
||||
|
||||
describe('when receiving an error', () => { |
||||
it('then it should call unsubscribe', () => { |
||||
const { subscription } = getTestContext({ error: 'An error' }); |
||||
|
||||
expect(subscription.unsubscribe).toHaveBeenCalledTimes(1); |
||||
}); |
||||
|
||||
it('then it should call reject', () => { |
||||
const { reject } = getTestContext({ error: 'An error' }); |
||||
|
||||
expect(reject).toHaveBeenCalledTimes(1); |
||||
expect(reject).toHaveBeenCalledWith('An error'); |
||||
}); |
||||
}); |
||||
|
||||
describe('when receiving complete', () => { |
||||
it('then it should call unsubscribe', () => { |
||||
const { subscription } = getTestContext({ complete: true }); |
||||
|
||||
expect(subscription.unsubscribe).toHaveBeenCalledTimes(1); |
||||
}); |
||||
|
||||
it('then it should call resolve', () => { |
||||
const { resolve } = getTestContext({ complete: true }); |
||||
|
||||
expect(resolve).toHaveBeenCalledTimes(1); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,36 @@ |
||||
import { Observer, Subscription } from 'rxjs'; |
||||
import { LoadingState } from '@grafana/data'; |
||||
|
||||
import { UpdateOptionsResults } from './VariableQueryRunner'; |
||||
|
||||
export function variableQueryObserver( |
||||
resolve: (value?: any) => void, |
||||
reject: (value?: any) => void, |
||||
subscription: Subscription |
||||
): Observer<UpdateOptionsResults> { |
||||
const observer: Observer<UpdateOptionsResults> = { |
||||
next: results => { |
||||
if (results.state === LoadingState.Error) { |
||||
subscription.unsubscribe(); |
||||
reject(results.error); |
||||
return; |
||||
} |
||||
|
||||
if (results.state === LoadingState.Done) { |
||||
subscription.unsubscribe(); |
||||
resolve(); |
||||
return; |
||||
} |
||||
}, |
||||
error: err => { |
||||
subscription.unsubscribe(); |
||||
reject(err); |
||||
}, |
||||
complete: () => { |
||||
subscription.unsubscribe(); |
||||
resolve(); |
||||
}, |
||||
}; |
||||
|
||||
return observer; |
||||
} |
||||
@ -0,0 +1,14 @@ |
||||
import { QueryVariableModel } from 'app/features/variables/types'; |
||||
import { DatasourceVariableBuilder } from './datasourceVariableBuilder'; |
||||
|
||||
export class QueryVariableBuilder<T extends QueryVariableModel> extends DatasourceVariableBuilder<T> { |
||||
withTags(useTags: boolean) { |
||||
this.variable.useTags = useTags; |
||||
return this; |
||||
} |
||||
|
||||
withTagsQuery(tagsQuery: string) { |
||||
this.variable.tagsQuery = tagsQuery; |
||||
return this; |
||||
} |
||||
} |
||||
@ -0,0 +1,31 @@ |
||||
import { from, Observable } from 'rxjs'; |
||||
import { map, mergeMap } from 'rxjs/operators'; |
||||
import { CustomVariableSupport, DataQueryRequest, DataQueryResponse } from '@grafana/data'; |
||||
|
||||
import CloudMonitoringDatasource from './datasource'; |
||||
import { CloudMonitoringVariableQuery } from './types'; |
||||
import CloudMonitoringMetricFindQuery from './CloudMonitoringMetricFindQuery'; |
||||
import { CloudMonitoringVariableQueryEditor } from './components/VariableQueryEditor'; |
||||
|
||||
export class CloudMonitoringVariableSupport extends CustomVariableSupport< |
||||
CloudMonitoringDatasource, |
||||
CloudMonitoringVariableQuery |
||||
> { |
||||
private readonly metricFindQuery: CloudMonitoringMetricFindQuery; |
||||
|
||||
constructor(private readonly datasource: CloudMonitoringDatasource) { |
||||
super(); |
||||
this.metricFindQuery = new CloudMonitoringMetricFindQuery(datasource); |
||||
this.query = this.query.bind(this); |
||||
} |
||||
|
||||
editor = CloudMonitoringVariableQueryEditor; |
||||
|
||||
query(request: DataQueryRequest<CloudMonitoringVariableQuery>): Observable<DataQueryResponse> { |
||||
const executeObservable = from(this.metricFindQuery.execute(request.targets[0])); |
||||
return from(this.datasource.ensureGCEDefaultProject()).pipe( |
||||
mergeMap(() => executeObservable), |
||||
map(data => ({ data })) |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,56 @@ |
||||
import { from, Observable, of } from 'rxjs'; |
||||
import { map } from 'rxjs/operators'; |
||||
import { |
||||
DataQueryRequest, |
||||
DataQueryResponse, |
||||
rangeUtil, |
||||
StandardVariableQuery, |
||||
StandardVariableSupport, |
||||
} from '@grafana/data'; |
||||
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; |
||||
|
||||
import { PrometheusDatasource } from './datasource'; |
||||
import { PromQuery } from './types'; |
||||
import PrometheusMetricFindQuery from './metric_find_query'; |
||||
import { getTimeSrv, TimeSrv } from '../../../features/dashboard/services/TimeSrv'; |
||||
|
||||
export class PrometheusVariableSupport extends StandardVariableSupport<PrometheusDatasource> { |
||||
constructor( |
||||
private readonly datasource: PrometheusDatasource, |
||||
private readonly templateSrv: TemplateSrv = getTemplateSrv(), |
||||
private readonly timeSrv: TimeSrv = getTimeSrv() |
||||
) { |
||||
super(); |
||||
this.query = this.query.bind(this); |
||||
} |
||||
|
||||
query(request: DataQueryRequest<PromQuery>): Observable<DataQueryResponse> { |
||||
const query = request.targets[0].expr; |
||||
if (!query) { |
||||
return of({ data: [] }); |
||||
} |
||||
|
||||
const scopedVars = { |
||||
...request.scopedVars, |
||||
__interval: { text: this.datasource.interval, value: this.datasource.interval }, |
||||
__interval_ms: { |
||||
text: rangeUtil.intervalToMs(this.datasource.interval), |
||||
value: rangeUtil.intervalToMs(this.datasource.interval), |
||||
}, |
||||
...this.datasource.getRangeScopedVars(this.timeSrv.timeRange()), |
||||
}; |
||||
|
||||
const interpolated = this.templateSrv.replace(query, scopedVars, this.datasource.interpolateQueryExpr); |
||||
const metricFindQuery = new PrometheusMetricFindQuery(this.datasource, interpolated); |
||||
const metricFindStream = from(metricFindQuery.process()); |
||||
|
||||
return metricFindStream.pipe(map(results => ({ data: results }))); |
||||
} |
||||
|
||||
toDataQuery(query: StandardVariableQuery): PromQuery { |
||||
return { |
||||
refId: 'PrometheusDatasource-VariableQuery', |
||||
expr: query.query, |
||||
}; |
||||
} |
||||
} |
||||
@ -0,0 +1,16 @@ |
||||
import { StandardVariableQuery, StandardVariableSupport } from '@grafana/data'; |
||||
|
||||
import { TestDataDataSource } from './datasource'; |
||||
import { TestDataQuery } from './types'; |
||||
|
||||
export class TestDataVariableSupport extends StandardVariableSupport<TestDataDataSource> { |
||||
toDataQuery(query: StandardVariableQuery): TestDataQuery { |
||||
return { |
||||
refId: 'TestDataDataSource-QueryVariable', |
||||
stringInput: query.query, |
||||
scenarioId: 'variables-query', |
||||
csvWave: null, |
||||
points: [], |
||||
}; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue