diff --git a/src/core/utils.ts b/src/core/utils.ts index 651285c..26aefcf 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -2,6 +2,7 @@ import { SelectableValue } from '@grafana/data'; import { useAsync } from 'react-use'; import { DataSourceBase } from './DataSourceBase'; import { Workspace } from './types'; +import { TemplateSrv } from '@grafana/runtime'; export function enumToOptions(stringEnum: { [name: string]: T }): Array> { const RESULT = []; @@ -62,3 +63,22 @@ export function getWorkspaceName(workspaces: Workspace[], id: string) { export function sleep(timeout: number) { return new Promise(res => window.setTimeout(res, timeout)); } + +/** + * Replace variables in an array of values. + * Useful for multi-value variables. + */ +export function replaceVariables(values: string[], templateSrv: TemplateSrv) { + const replaced: string[] = []; + values.forEach((col: string, index) => { + let value = col; + if (templateSrv.containsTemplate(col)) { + const variables = templateSrv.getVariables() as any[]; + const variable = variables.find(v => v.name === col.split('$')[1]); + value = variable.current.value; + } + replaced.push(value); + }); + // Dedupe and flatten + return [...new Set(replaced.flat())]; +} diff --git a/src/datasources/data-frame/DataFrameDataSource.test.ts b/src/datasources/data-frame/DataFrameDataSource.test.ts index 5d68cee..1277938 100644 --- a/src/datasources/data-frame/DataFrameDataSource.test.ts +++ b/src/datasources/data-frame/DataFrameDataSource.test.ts @@ -8,11 +8,21 @@ import { DataFrameDataSource } from './DataFrameDataSource'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => ({ fetch: fetchMock }), - getTemplateSrv: () => ({ replace: replaceMock }), + getTemplateSrv: () => ({ replace: replaceMock, containsTemplate: containsTemplateMock, getVariables: getVariablesMock }), })); +const mockVariables = [{ + name: 'tableId', + current: { value: '1' } +}, { + name: 'columns', + current: { value: ['time', 'int'] } +}]; + const fetchMock = jest.fn, [BackendSrvRequest]>(); const replaceMock = jest.fn((a: string, ...rest: any) => a); +const containsTemplateMock = jest.fn((a: string) => mockVariables.map(v => `$${v.name}`).includes(a)); +const getVariablesMock = jest.fn(() => mockVariables); let ds: DataFrameDataSource; @@ -205,13 +215,25 @@ it('attempts to replace variables in metadata query', async () => { }); it('attempts to replace variables in data query', async () => { - const query = buildQuery([{ refId: 'A', type: DataFrameQueryType.Data, tableId: '$tableId', columns: ['float'] }]); + const query = buildQuery([{ refId: 'A', type: DataFrameQueryType.Data, tableId: '$tableId', columns: ['$columns'] }]); replaceMock.mockReturnValueOnce('1'); await ds.query(query); expect(replaceMock).toHaveBeenCalledTimes(2); expect(replaceMock).toHaveBeenCalledWith(query.targets[0].tableId, expect.anything()); + expect(containsTemplateMock).toHaveBeenCalledTimes(1); + expect(containsTemplateMock).toHaveBeenCalledWith(query.targets[0].columns![0]); +}); + +it('metricFindQuery returns table columns', async () => { + const tableId = '1'; + const expectedColumns = fakeMetadataResponse.columns.map(col => ({ text: col.name, value: col.name })); + + const columns = await ds.metricFindQuery({ tableId } as DataFrameQuery); + + expect(fetchMock).toHaveBeenCalledWith(expect.objectContaining({ url: `_/nidataframe/v1/tables/${tableId}` })); + expect(columns).toEqual(expect.arrayContaining(expectedColumns)); }); it('returns table properties for metadata query', async () => { diff --git a/src/datasources/data-frame/DataFrameDataSource.ts b/src/datasources/data-frame/DataFrameDataSource.ts index 6c249a5..2cee8a4 100644 --- a/src/datasources/data-frame/DataFrameDataSource.ts +++ b/src/datasources/data-frame/DataFrameDataSource.ts @@ -1,6 +1,6 @@ import TTLCache from '@isaacs/ttlcache'; import deepEqual from 'fast-deep-equal'; -import { DataQueryRequest, DataSourceInstanceSettings, FieldType, TimeRange, FieldDTO, dateTime, DataFrameDTO } from '@grafana/data'; +import { DataQueryRequest, DataSourceInstanceSettings, FieldType, TimeRange, FieldDTO, dateTime, DataFrameDTO, MetricFindValue } from '@grafana/data'; import { BackendSrv, TemplateSrv, TestingStatus, getBackendSrv, getTemplateSrv } from '@grafana/runtime'; import { ColumnDataType, @@ -17,6 +17,7 @@ import { import { metadataCacheTTL } from './constants'; import _ from 'lodash'; import { DataSourceBase } from 'core/DataSourceBase'; +import { replaceVariables } from 'core/utils'; export class DataFrameDataSource extends DataSourceBase { private readonly metadataCache: TTLCache = new TTLCache({ ttl: metadataCacheTTL }); @@ -35,7 +36,8 @@ export class DataFrameDataSource extends DataSourceBase { async runQuery(query: DataFrameQuery, { range, scopedVars, maxDataPoints }: DataQueryRequest): Promise { const processedQuery = this.processQuery(query); - processedQuery.tableId = getTemplateSrv().replace(processedQuery.tableId, scopedVars); + processedQuery.tableId = this.templateSrv.replace(processedQuery.tableId, scopedVars); + processedQuery.columns = replaceVariables(processedQuery.columns, this.templateSrv); const metadata = await this.getTableMetadata(processedQuery.tableId); if (processedQuery.type === DataFrameQueryType.Metadata) { @@ -63,7 +65,7 @@ export class DataFrameDataSource extends DataSourceBase { } async getTableMetadata(id?: string): Promise { - const resolvedId = getTemplateSrv().replace(id); + const resolvedId = this.templateSrv.replace(id); let metadata = this.metadataCache.get(resolvedId); if (!metadata) { @@ -119,6 +121,11 @@ export class DataFrameDataSource extends DataSourceBase { return deepEqual(migratedQuery, query) ? (query as ValidDataFrameQuery) : migratedQuery; } + async metricFindQuery(tableQuery: DataFrameQuery): Promise { + const tableMetadata = await this.getTableMetadata(tableQuery.tableId); + return tableMetadata.columns.map(col => ({ text: col.name, value: col.name })); + } + private getColumnTypes(columnNames: string[], tableMetadata: Column[]): Column[] { return columnNames.map(c => { const column = tableMetadata.find(({ name }) => name === c); diff --git a/src/datasources/data-frame/components/DataFrameQueryEditor.tsx b/src/datasources/data-frame/components/DataFrameQueryEditor.tsx index b27d552..da98c28 100644 --- a/src/datasources/data-frame/components/DataFrameQueryEditor.tsx +++ b/src/datasources/data-frame/components/DataFrameQueryEditor.tsx @@ -1,83 +1,39 @@ import React, { useState } from 'react'; import { useAsync } from 'react-use'; -import { CoreApp, QueryEditorProps, SelectableValue, toOption } from '@grafana/data'; -import { DataFrameDataSource } from '../DataFrameDataSource'; -import { DataFrameQuery, DataFrameQueryType } from '../types'; -import { - InlineField, - InlineSwitch, - MultiSelect, - Select, - AsyncSelect, - LoadOptionsCallback, - RadioButtonGroup, -} from '@grafana/ui'; +import { SelectableValue, toOption } from '@grafana/data'; +import { InlineField, InlineSwitch, MultiSelect, Select, AsyncSelect, RadioButtonGroup } from '@grafana/ui'; import { decimationMethods } from '../constants'; import _ from 'lodash'; import { getTemplateSrv } from '@grafana/runtime'; import { isValidId } from '../utils'; import { FloatingError, parseErrorMessage } from '../errors'; -import { enumToOptions, getWorkspaceName } from 'core/utils'; - -type Props = QueryEditorProps; +import { DataFrameQueryEditorCommon, Props } from './DataFrameQueryEditorCommon'; +import { enumToOptions } from 'core/utils'; +import { DataFrameQueryType } from '../types'; export const DataFrameQueryEditor = (props: Props) => { - const { datasource, onChange } = props; - const query = datasource.processQuery(props.query); - const onRunQuery = () => props.app !== CoreApp.Explore && props.onRunQuery(); - const [errorMsg, setErrorMsg] = useState(''); const handleError = (error: Error) => setErrorMsg(parseErrorMessage(error)); - - const tableMetadata = useAsync(() => datasource.getTableMetadata(query.tableId).catch(handleError), [query.tableId]); - - const handleQueryChange = (value: DataFrameQuery, runQuery: boolean) => { - onChange(value); - if (runQuery) { - onRunQuery(); - } - }; - - const handleIdChange = (item: SelectableValue) => { - if (query.tableId !== item.value) { - handleQueryChange({ ...query, tableId: item.value, columns: [] }, query.type === DataFrameQueryType.Metadata); - } - }; + const common = new DataFrameQueryEditorCommon(props, handleError); + const tableMetadata = useAsync(() => common.datasource.getTableMetadata(common.query.tableId).catch(handleError), [common.query.tableId]); const handleColumnChange = (items: Array>) => { - handleQueryChange({ ...query, columns: items.map(i => i.value!) }, false); + common.handleQueryChange({ ...common.query, columns: items.map(i => i.value!) }, false); }; - const loadTableOptions = _.debounce((query: string, cb?: LoadOptionsCallback) => { - Promise.all([datasource.queryTables(query), datasource.getWorkspaces()]) - .then(([tables, workspaces]) => - cb?.( - tables.map(t => ({ - label: t.name, - value: t.id, - title: t.id, - description: getWorkspaceName(workspaces, t.workspace), - })) - ) - ) - .catch(handleError); - }, 300); - - const handleLoadOptions = (query: string, cb?: LoadOptionsCallback) => { - if (!query || query.startsWith('$')) { - return cb?.(getVariableOptions().filter(v => v.value?.includes(query))); - } - - loadTableOptions(query, cb); - }; + const loadColumnOptions = () => { + const columnOptions = (tableMetadata.value?.columns ?? []).map(c => toOption(c.name)); + columnOptions.unshift(...getVariableOptions()); + return columnOptions; + } return (
handleQueryChange({ ...query, type: value }, true)} + value={common.query.type} + onChange={value => common.handleQueryChange({ ...common.query, type: value }, true)} /> @@ -87,41 +43,41 @@ export const DataFrameQueryEditor = (props: Props) => { cacheOptions={false} defaultOptions isValidNewOption={isValidId} - loadOptions={handleLoadOptions} - onChange={handleIdChange} + loadOptions={common.handleLoadOptions} + onChange={common.handleIdChange} placeholder="Search by name or enter id" width={30} - value={query.tableId ? toOption(query.tableId) : null} + value={common.query.tableId ? toOption(common.query.tableId) : null} /> - {query.type === DataFrameQueryType.Data && ( + {common.query.type === DataFrameQueryType.Data && ( <> toOption(c.name))} + options={loadColumnOptions()} onChange={handleColumnChange} - onBlur={onRunQuery} - value={query.columns.map(toOption)} + onBlur={common.onRunQuery} + value={common.query.columns.map(toOption)} />