From 83de6d0a4f1a08d363951fd0b8899cd7db35affc Mon Sep 17 00:00:00 2001 From: Carson Moore Date: Tue, 31 Oct 2023 17:32:19 -0500 Subject: [PATCH 1/4] feat(dataframe): visualize table properties --- src/core/DataSourceBase.ts | 11 +- .../data-frame/DataFrameDataSource.test.ts | 77 +++++++---- .../data-frame/DataFrameDataSource.ts | 122 +++++++----------- .../components/DataFrameQueryEditor.tsx | 115 +++++++++++------ src/datasources/data-frame/types.ts | 9 +- 5 files changed, 185 insertions(+), 149 deletions(-) diff --git a/src/core/DataSourceBase.ts b/src/core/DataSourceBase.ts index ef15623..c83dfb1 100644 --- a/src/core/DataSourceBase.ts +++ b/src/core/DataSourceBase.ts @@ -1,18 +1,11 @@ import { - DataFrame, DataFrameDTO, DataQueryRequest, DataQueryResponse, DataSourceApi, DataSourceInstanceSettings, } from '@grafana/data'; -import { - BackendSrv, - BackendSrvRequest, - TemplateSrv, - TestingStatus, - isFetchError -} from '@grafana/runtime'; +import { BackendSrv, BackendSrvRequest, TemplateSrv, TestingStatus, isFetchError } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { Workspace } from './types'; import { sleep } from './utils'; @@ -28,7 +21,7 @@ export abstract class DataSourceBase extends DataSourc } abstract defaultQuery: Partial & Omit; - abstract runQuery(query: TQuery, options: DataQueryRequest): Promise; + abstract runQuery(query: TQuery, options: DataQueryRequest): Promise; abstract shouldRunQuery(query: TQuery): boolean; abstract testDatasource(): Promise; diff --git a/src/datasources/data-frame/DataFrameDataSource.test.ts b/src/datasources/data-frame/DataFrameDataSource.test.ts index 4ede3ba..66bd39a 100644 --- a/src/datasources/data-frame/DataFrameDataSource.test.ts +++ b/src/datasources/data-frame/DataFrameDataSource.test.ts @@ -2,7 +2,7 @@ import { of, Observable } from 'rxjs'; import { DataQueryRequest, DataSourceInstanceSettings, dateTime, Field, FieldType } from '@grafana/data'; import { BackendSrvRequest, FetchResponse } from '@grafana/runtime'; -import { DataFrameQuery, TableDataRows, TableMetadata } from './types'; +import { DataFrameQuery, DataFrameQueryType, TableDataRows, TableMetadata } from './types'; import { DataFrameDataSource } from './DataFrameDataSource'; jest.mock('@grafana/runtime', () => ({ @@ -28,8 +28,8 @@ beforeEach(() => { it('should return no data if there are no valid queries', async () => { const query = buildQuery([ - { refId: 'A' }, // initial state when creating a panel - { refId: 'B', tableId: '_', columns: [] }, // state after entering a table id, but no columns selected + { refId: 'A', type: DataFrameQueryType.Data }, // initial state when creating a panel + { refId: 'B', type: DataFrameQueryType.Data, tableId: '_', columns: [] }, // state after entering a table id, but no columns selected ]); const response = await ds.query(query); @@ -39,8 +39,8 @@ it('should return no data if there are no valid queries', async () => { it('should return data ignoring invalid queries', async () => { const query = buildQuery([ - { refId: 'A', tableId: '_' }, // invalid - { refId: 'B', tableId: '1', columns: ['float'] }, + { refId: 'A', type: DataFrameQueryType.Data, tableId: '_' }, // invalid + { refId: 'B', type: DataFrameQueryType.Data, tableId: '1', columns: ['float'] }, ]); await ds.query(query); @@ -51,8 +51,8 @@ it('should return data ignoring invalid queries', async () => { it('should return data for multiple targets', async () => { const query = buildQuery([ - { refId: 'A', tableId: '1', columns: ['int'] }, - { refId: 'B', tableId: '2', columns: ['float'] }, + { refId: 'A', type: DataFrameQueryType.Data, tableId: '1', columns: ['int'] }, + { refId: 'B', type: DataFrameQueryType.Data, tableId: '2', columns: ['float'] }, ]); const response = await ds.query(query); @@ -65,6 +65,7 @@ it('should convert columns to Grafana fields', async () => { const query = buildQuery([ { refId: 'A', + type: DataFrameQueryType.Data, tableId: '_', columns: ['int', 'float', 'string', 'time', 'bool', 'Value'], }, @@ -73,13 +74,12 @@ it('should convert columns to Grafana fields', async () => { const response = await ds.query(query); const fields = response.data[0].fields as Field[]; - const actual = fields.map(({ name, type, values, config }) => ({ name, type, values: values.toArray(), config })); - expect(actual).toEqual([ - { name: 'int', type: FieldType.number, values: [1, 2], config: {} }, - { name: 'float', type: FieldType.number, values: [1.1, 2.2], config: {} }, - { name: 'string', type: FieldType.string, values: ['first', 'second'], config: {} }, - { name: 'time', type: FieldType.time, values: [1663135260000, 1663135320000], config: {} }, - { name: 'bool', type: FieldType.boolean, values: [true, false], config: {} }, + expect(fields).toEqual([ + { name: 'int', type: FieldType.number, values: [1, 2] }, + { name: 'float', type: FieldType.number, values: [1.1, 2.2] }, + { name: 'string', type: FieldType.string, values: ['first', 'second'] }, + { name: 'time', type: FieldType.time, values: [1663135260000, 1663135320000] }, + { name: 'bool', type: FieldType.boolean, values: [true, false] }, { name: 'Value', type: FieldType.string, values: ['test1', 'test2'], config: { displayName: 'Value' } }, ]); }); @@ -88,6 +88,7 @@ it('should automatically apply time filters when index column is a timestamp', a const query = buildQuery([ { refId: 'A', + type: DataFrameQueryType.Data, tableId: '_', columns: ['time'], applyTimeFilters: true, @@ -115,6 +116,7 @@ it('should apply null and NaN filters', async () => { const query = buildQuery([ { refId: 'A', + type: DataFrameQueryType.Data, tableId: '_', columns: ['int', 'float', 'string'], filterNulls: true, @@ -140,6 +142,7 @@ it('should provide decimation parameters correctly', async () => { const query = buildQuery([ { refId: 'A', + type: DataFrameQueryType.Data, tableId: '_', columns: ['int', 'string', 'float'], decimationMethod: 'ENTRY_EXIT', @@ -159,7 +162,7 @@ it('should provide decimation parameters correctly', async () => { }); it('should cache table metadata for subsequent requests', async () => { - const query = buildQuery([{ refId: 'A', tableId: '1', columns: ['int'] }]); + const query = buildQuery([{ refId: 'A', type: DataFrameQueryType.Data, tableId: '1', columns: ['int'] }]); await ds.query(query); @@ -172,7 +175,7 @@ it('should cache table metadata for subsequent requests', async () => { }); it('should return error if query columns do not match table metadata', async () => { - const query = buildQuery([{ refId: 'A', tableId: '1', columns: ['nonexistent'] }]); + const query = buildQuery([{ refId: 'A', type: DataFrameQueryType.Data, tableId: '1', columns: ['nonexistent'] }]); await expect(ds.query(query)).rejects.toEqual(expect.anything()); }); @@ -202,7 +205,7 @@ it('attempts to replace variables in metadata query', async () => { }); it('attempts to replace variables in data query', async () => { - const query = buildQuery([{ refId: 'A', tableId: '$tableId', columns: ['float'] }]); + const query = buildQuery([{ refId: 'A', type: DataFrameQueryType.Data, tableId: '$tableId', columns: ['float'] }]); replaceMock.mockReturnValue('1'); await ds.query(query); @@ -211,6 +214,18 @@ it('attempts to replace variables in data query', async () => { expect(replaceMock).toHaveBeenCalledWith(query.targets[0].tableId, expect.anything()); }); +it('returns table properties for metadata query', async () => { + const query = buildQuery([{ refId: 'A', type: DataFrameQueryType.Metadata, tableId: '1' }]); + + const response = await ds.query(query); + + expect(fetchMock).toHaveBeenCalledWith(expect.objectContaining({ url: '_/nidataframe/v1/tables/1' })); + expect(response.data[0].fields).toEqual([ + { name: 'name', values: ['hello', 'foo'] }, + { name: 'value', values: ['world', 'bar'] }, + ]) +}); + const buildQuery = (targets: DataFrameQuery[]): DataQueryRequest => { return { ...defaultQuery, @@ -224,7 +239,7 @@ const setupFetchMock = () => { return of(createFetchResponse(fakeMetadataResponse)); } if (/\/tables\/\w+\/query-decimated-data$/.test(options.url)) { - return of(createFetchResponse(fakeDataResponse)); + return of(createFetchResponse(getFakeDataResponse(options.data.columns))); } throw new Error('Unexpected request'); @@ -255,19 +270,27 @@ const fakeMetadataResponse: TableMetadata = { { name: 'Value', dataType: 'STRING', columnType: 'NULLABLE', properties: {} }, ], id: '_', + properties: { hello: 'world', foo: 'bar' }, name: 'Test Table', workspace: '_', }; -const fakeDataResponse: TableDataRows = { - frame: { - columns: ['int', 'float', 'string', 'time', 'bool', 'Value'], - data: [ - ['1', '1.1', 'first', '2022-09-14T06:01:00.0000000Z', 'True', 'test1'], - ['2', '2.2', 'second', '2022-09-14T06:02:00.0000000Z', 'False', 'test2'], - ], - }, - continuationToken: '_', +const fakeData: Record = { + int: ['1', '2'], + float: ['1.1', '2.2'], + string: ['first', 'second'], + time: ['2022-09-14T06:01:00.0000000Z', '2022-09-14T06:02:00.0000000Z'], + bool: ['True', 'False'], + Value: ['test1', 'test2'], +}; + +function getFakeDataResponse(columns: string[]): TableDataRows { + return { + frame: { + columns, + data: [columns.map(c => fakeData[c][0]), columns.map(c => fakeData[c][1])] + } + } }; const defaultQuery: DataQueryRequest = { diff --git a/src/datasources/data-frame/DataFrameDataSource.ts b/src/datasources/data-frame/DataFrameDataSource.ts index 8b1e14e..88436bb 100644 --- a/src/datasources/data-frame/DataFrameDataSource.ts +++ b/src/datasources/data-frame/DataFrameDataSource.ts @@ -1,22 +1,7 @@ import TTLCache from '@isaacs/ttlcache'; import deepEqual from 'fast-deep-equal'; -import { - DataQueryRequest, - DataSourceInstanceSettings, - toDataFrame, - TableData, - FieldType, - standardTransformers, - DataFrame, - TimeRange -} from '@grafana/data'; -import { - BackendSrv, - TemplateSrv, - TestingStatus, - getBackendSrv, - getTemplateSrv -} from '@grafana/runtime'; +import { DataQueryRequest, DataSourceInstanceSettings, FieldType, TimeRange, FieldDTO, dateTime } from '@grafana/data'; +import { BackendSrv, TemplateSrv, TestingStatus, getBackendSrv, getTemplateSrv } from '@grafana/runtime'; import { ColumnDataType, DataFrameQuery, @@ -27,6 +12,7 @@ import { Column, defaultQuery, ValidDataFrameQuery, + DataFrameQueryType, } from './types'; import { metadataCacheTTL } from './constants'; import _ from 'lodash'; @@ -45,32 +31,39 @@ export class DataFrameDataSource extends DataSourceBase { baseUrl = this.instanceSettings.url + '/nidataframe/v1'; - defaultQuery = defaultQuery + defaultQuery = defaultQuery; - async runQuery(query: DataFrameQuery, { range, scopedVars, maxDataPoints }: DataQueryRequest): Promise { + async runQuery(query: DataFrameQuery, { range, scopedVars, maxDataPoints }: DataQueryRequest) { const processedQuery = this.processQuery(query); processedQuery.tableId = getTemplateSrv().replace(processedQuery.tableId, scopedVars); - const tableMetadata = await this.getTableMetadata(processedQuery.tableId); - const columns = this.getColumnTypes(processedQuery.columns, tableMetadata?.columns ?? []); - const tableData = await this.getDecimatedTableData(processedQuery, columns, range, maxDataPoints); - return this.convertDataFrameFields({ - refId: processedQuery.refId, - name: processedQuery.tableId, - columns: processedQuery.columns.map((name) => ({ text: name })), - rows: tableData.frame.data, - } as TableData, columns); + const metadata = await this.getTableMetadata(processedQuery.tableId); + + if (processedQuery.type === DataFrameQueryType.Metadata) { + return { + refId: processedQuery.refId, + name: metadata.name, + fields: [ + { name: 'name', values: Object.keys(metadata.properties) }, + { name: 'value', values: Object.values(metadata.properties) }, + ], + }; + } else { + const columns = this.getColumnTypes(processedQuery.columns, metadata?.columns ?? []); + const tableData = await this.getDecimatedTableData(processedQuery, columns, range, maxDataPoints); + return { + refId: processedQuery.refId, + name: metadata.name, + fields: this.dataFrameToFields(tableData.frame.data, columns), + }; + } } shouldRunQuery(query: ValidDataFrameQuery): boolean { - return Boolean(query.tableId) && Boolean(query.columns.length); + return Boolean(query.tableId) && (query.type === DataFrameQueryType.Metadata || Boolean(query.columns.length)); } - async getTableMetadata(id?: string): Promise { + async getTableMetadata(id?: string): Promise { const resolvedId = getTemplateSrv().replace(id); - if (!resolvedId) { - return null; - } - let metadata = this.metadataCache.get(resolvedId); if (!metadata) { @@ -98,7 +91,7 @@ export class DataFrameDataSource extends DataSourceBase { decimation: { intervals, method: query.decimationMethod, - yColumns: this.getNumericColumns(columns).map((c) => c.name), + yColumns: this.getNumericColumns(columns).map(c => c.name), }, }); } @@ -106,7 +99,7 @@ export class DataFrameDataSource extends DataSourceBase { async queryTables(query: string): Promise { const filter = `name.Contains("${query}")`; - return (await this.post(`${this.baseUrl}/query-tables`, { filter, take: 5 })).tables; + return (await this.post(`${this.baseUrl}/query-tables`, { filter, take: 5 })).tables; } async testDatasource(): Promise { @@ -119,15 +112,15 @@ export class DataFrameDataSource extends DataSourceBase { // Migration for 1.6.0: DataFrameQuery.columns changed to string[] if (_.isObject(migratedQuery.columns[0])) { - migratedQuery.columns = (migratedQuery.columns as any[]).map((c) => c.name); + migratedQuery.columns = (migratedQuery.columns as any[]).map(c => c.name); } // If we didn't make any changes to the query, then return the original object - return deepEqual(migratedQuery, query) ? query as ValidDataFrameQuery : migratedQuery; - }; + return deepEqual(migratedQuery, query) ? (query as ValidDataFrameQuery) : migratedQuery; + } private getColumnTypes(columnNames: string[], tableMetadata: Column[]): Column[] { - return columnNames.map((c) => { + return columnNames.map(c => { const column = tableMetadata.find(({ name }) => name === c); if (!column) { @@ -138,52 +131,33 @@ export class DataFrameDataSource extends DataSourceBase { }); } - private getFieldType(dataType: ColumnDataType): FieldType { + private getFieldTypeAndConverter(dataType: ColumnDataType): [FieldType, (v: string) => any] { switch (dataType) { case 'BOOL': - return FieldType.boolean; + return [FieldType.boolean, v => v.toLowerCase() === 'true']; case 'STRING': - return FieldType.string; + return [FieldType.string, v => v]; case 'TIMESTAMP': - return FieldType.time; + return [FieldType.time, v => dateTime(v).valueOf()]; default: - return FieldType.number; + return [FieldType.number, v => Number(v)]; } } - private convertDataFrameFields(tableData: TableData, columns: Column[]): DataFrame { - this.transformBooleanFields(tableData, columns); - const frame = toDataFrame(tableData); - frame.fields.forEach(field => { - if (field.name.toLowerCase() === 'value') { - field.config.displayName = field.name; - } - }) - const transformer = standardTransformers.convertFieldTypeTransformer.transformer; - const conversions = columns.map(({ name, dataType }) => ({ - targetField: name, - destinationType: this.getFieldType(dataType), - dateFormat: 'YYYY-MM-DDTHH:mm:ss.SZ', - })); - return transformer({ conversions }, { interpolate: _.identity })([frame])[0]; - } - - private transformBooleanFields(tableData: TableData, columns: Column[]): void { - const boolColumnIndices: number[] = []; - columns.forEach((column, i) => { - if (column.dataType === 'BOOL') { - boolColumnIndices.push(i); - } + private dataFrameToFields(rows: string[][], columns: Column[]): FieldDTO[] { + return columns.map((col, ix) => { + const [type, converter] = this.getFieldTypeAndConverter(col.dataType); + return { + name: col.name, + type, + values: rows.map(row => (row[ix] !== null ? converter(row[ix]) : null)), + ...(col.name.toLowerCase() === 'value' && { config: { displayName: col.name } }), + }; }); - if (!!boolColumnIndices.length) { - tableData.rows.forEach(row => { - boolColumnIndices.forEach(i => row[i] = row[i].toLowerCase() === 'true'); - }) - } } private constructTimeFilters(columns: Column[], timeRange: TimeRange): ColumnFilter[] { - const timeIndex = columns.find((c) => c.dataType === 'TIMESTAMP' && c.columnType === 'INDEX'); + const timeIndex = columns.find(c => c.dataType === 'TIMESTAMP' && c.columnType === 'INDEX'); if (!timeIndex) { return []; diff --git a/src/datasources/data-frame/components/DataFrameQueryEditor.tsx b/src/datasources/data-frame/components/DataFrameQueryEditor.tsx index e84f393..1d3faeb 100644 --- a/src/datasources/data-frame/components/DataFrameQueryEditor.tsx +++ b/src/datasources/data-frame/components/DataFrameQueryEditor.tsx @@ -2,14 +2,22 @@ import React, { useState } from 'react'; import { useAsync } from 'react-use'; import { CoreApp, QueryEditorProps, SelectableValue, toOption } from '@grafana/data'; import { DataFrameDataSource } from '../DataFrameDataSource'; -import { DataFrameQuery } from '../types'; -import { InlineField, InlineSwitch, MultiSelect, Select, AsyncSelect, LoadOptionsCallback } from '@grafana/ui'; +import { DataFrameQuery, DataFrameQueryType } from '../types'; +import { + InlineField, + InlineSwitch, + MultiSelect, + Select, + AsyncSelect, + LoadOptionsCallback, + 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 { getWorkspaceName } from 'core/utils'; +import { enumToOptions, getWorkspaceName } from 'core/utils'; type Props = QueryEditorProps; @@ -32,7 +40,7 @@ export const DataFrameQueryEditor = (props: Props) => { const handleIdChange = (item: SelectableValue) => { if (query.tableId !== item.value) { - handleQueryChange({ ...query, tableId: item.value, columns: [] }, false); + handleQueryChange({ ...query, tableId: item.value, columns: [] }, query.type === DataFrameQueryType.Metadata); } }; @@ -42,13 +50,22 @@ export const DataFrameQueryEditor = (props: Props) => { 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) })))) + .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))); + return cb?.(getVariableOptions().filter(v => v.value?.includes(query))); } loadTableOptions(query, cb); @@ -56,6 +73,13 @@ export const DataFrameQueryEditor = (props: Props) => { return (
+ + handleQueryChange({ ...query, type: value }, true)} + /> + { value={query.tableId ? toOption(query.tableId) : null} /> - - toOption(c.name))} - onChange={handleColumnChange} - onBlur={onRunQuery} - value={(query.columns).map(toOption)} - /> - - - handleQueryChange({ ...query, decimationMethod: item.value! }, true)} + value={query.decimationMethod} + /> + + + handleQueryChange({ ...query, filterNulls: event.currentTarget.checked }, true)} + > + + + handleQueryChange({ ...query, applyTimeFilters: event.currentTarget.checked }, true)} + > + + + )}
); @@ -109,5 +134,19 @@ export const DataFrameQueryEditor = (props: Props) => { const getVariableOptions = () => { return getTemplateSrv() .getVariables() - .map((v) => toOption('$' + v.name)); + .map(v => toOption('$' + v.name)); +}; + +const tooltips = { + queryType: `Data allows you to visualize the rows of data in a table. Metadata allows you + to visualize the properties associated with a table.`, + + columns: `Specifies the columns to include in the response data.`, + + decimation: `Specifies the method used to decimate the data.`, + + filterNulls: `Filters out null and NaN values before decimating the data.`, + + useTimeRange: `Queries only for data within the dashboard time range if the table index is a + timestamp. Enable when interacting with your data on a graph.`, }; diff --git a/src/datasources/data-frame/types.ts b/src/datasources/data-frame/types.ts index 5b974e1..2047e61 100644 --- a/src/datasources/data-frame/types.ts +++ b/src/datasources/data-frame/types.ts @@ -1,6 +1,12 @@ import { DataQuery } from '@grafana/schema'; +export enum DataFrameQueryType { + Data = 'Data', + Metadata = 'Metadata', +} + export interface DataFrameQuery extends DataQuery { + type: DataFrameQueryType; tableId?: string; columns?: string[]; decimationMethod?: string; @@ -9,6 +15,7 @@ export interface DataFrameQuery extends DataQuery { } export const defaultQuery: Omit = { + type: DataFrameQueryType.Data, tableId: '', columns: [], decimationMethod: 'LOSSY', @@ -46,6 +53,7 @@ export interface TableMetadata { id: string; name: string; workspace: string; + properties: Record; } export interface TableMetadataList { @@ -55,7 +63,6 @@ export interface TableMetadataList { export interface TableDataRows { frame: { columns: string[]; data: string[][] }; - continuationToken: string; } export interface SystemLinkError { From 623e2858248d8bdc391cbe6d2dff0f154dcc329b Mon Sep 17 00:00:00 2001 From: Carson Moore Date: Wed, 1 Nov 2023 11:13:24 -0500 Subject: [PATCH 2/4] Add no properties test --- .../data-frame/DataFrameDataSource.test.ts | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/datasources/data-frame/DataFrameDataSource.test.ts b/src/datasources/data-frame/DataFrameDataSource.test.ts index 66bd39a..5d68cee 100644 --- a/src/datasources/data-frame/DataFrameDataSource.test.ts +++ b/src/datasources/data-frame/DataFrameDataSource.test.ts @@ -52,7 +52,7 @@ it('should return data ignoring invalid queries', async () => { it('should return data for multiple targets', async () => { const query = buildQuery([ { refId: 'A', type: DataFrameQueryType.Data, tableId: '1', columns: ['int'] }, - { refId: 'B', type: DataFrameQueryType.Data, tableId: '2', columns: ['float'] }, + { refId: 'B', type: DataFrameQueryType.Data, tableId: '1', columns: ['float'] }, ]); const response = await ds.query(query); @@ -66,7 +66,7 @@ it('should convert columns to Grafana fields', async () => { { refId: 'A', type: DataFrameQueryType.Data, - tableId: '_', + tableId: '1', columns: ['int', 'float', 'string', 'time', 'bool', 'Value'], }, ]); @@ -89,7 +89,7 @@ it('should automatically apply time filters when index column is a timestamp', a { refId: 'A', type: DataFrameQueryType.Data, - tableId: '_', + tableId: '1', columns: ['time'], applyTimeFilters: true, }, @@ -117,7 +117,7 @@ it('should apply null and NaN filters', async () => { { refId: 'A', type: DataFrameQueryType.Data, - tableId: '_', + tableId: '1', columns: ['int', 'float', 'string'], filterNulls: true, }, @@ -143,7 +143,7 @@ it('should provide decimation parameters correctly', async () => { { refId: 'A', type: DataFrameQueryType.Data, - tableId: '_', + tableId: '1', columns: ['int', 'string', 'float'], decimationMethod: 'ENTRY_EXIT', }, @@ -196,7 +196,7 @@ it('should migrate queries using columns of arrays of objects', async () => { it('attempts to replace variables in metadata query', async () => { const tableId = '$tableId'; - replaceMock.mockReturnValue('1'); + replaceMock.mockReturnValueOnce('1'); await ds.getTableMetadata(tableId); @@ -206,7 +206,7 @@ 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'] }]); - replaceMock.mockReturnValue('1'); + replaceMock.mockReturnValueOnce('1'); await ds.query(query); @@ -226,6 +226,19 @@ it('returns table properties for metadata query', async () => { ]) }); +it('handles metadata query when table has no properties', async () => { + const query = buildQuery([{ refId: 'A', type: DataFrameQueryType.Metadata, tableId: '2' }]); + + const response = await ds.query(query); + + console.log(fetchMock.mock.calls) + expect(fetchMock).toHaveBeenCalledWith(expect.objectContaining({ url: '_/nidataframe/v1/tables/2' })); + expect(response.data[0].fields).toEqual([ + { name: 'name', values: [] }, + { name: 'value', values: [] }, + ]) +}); + const buildQuery = (targets: DataFrameQuery[]): DataQueryRequest => { return { ...defaultQuery, @@ -235,9 +248,14 @@ const buildQuery = (targets: DataFrameQuery[]): DataQueryRequest const setupFetchMock = () => { fetchMock.mockImplementation((options: BackendSrvRequest) => { - if (/\/tables\/\w+$/.test(options.url)) { + if (/\/tables\/1$/.test(options.url)) { return of(createFetchResponse(fakeMetadataResponse)); } + + if (/\/tables\/2$/.test(options.url)) { + return of(createFetchResponse(fakeMetadataResponseNoProperties)); + } + if (/\/tables\/\w+\/query-decimated-data$/.test(options.url)) { return of(createFetchResponse(getFakeDataResponse(options.data.columns))); } @@ -275,6 +293,14 @@ const fakeMetadataResponse: TableMetadata = { workspace: '_', }; +const fakeMetadataResponseNoProperties: TableMetadata = { + columns: [{ name: 'time', dataType: 'TIMESTAMP', columnType: 'INDEX', properties: {} }], + id: '_', + properties: {}, + name: 'Test Table no properties', + workspace: '_', +}; + const fakeData: Record = { int: ['1', '2'], float: ['1.1', '2.2'], From be8b1215a37e1b4c8f803f3404387dc34f8063bb Mon Sep 17 00:00:00 2001 From: Carson Moore Date: Wed, 1 Nov 2023 12:04:41 -0500 Subject: [PATCH 3/4] add back annotation --- src/datasources/data-frame/DataFrameDataSource.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/datasources/data-frame/DataFrameDataSource.ts b/src/datasources/data-frame/DataFrameDataSource.ts index 88436bb..6c249a5 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 } from '@grafana/data'; +import { DataQueryRequest, DataSourceInstanceSettings, FieldType, TimeRange, FieldDTO, dateTime, DataFrameDTO } from '@grafana/data'; import { BackendSrv, TemplateSrv, TestingStatus, getBackendSrv, getTemplateSrv } from '@grafana/runtime'; import { ColumnDataType, @@ -33,7 +33,7 @@ export class DataFrameDataSource extends DataSourceBase { defaultQuery = defaultQuery; - async runQuery(query: DataFrameQuery, { range, scopedVars, maxDataPoints }: DataQueryRequest) { + async runQuery(query: DataFrameQuery, { range, scopedVars, maxDataPoints }: DataQueryRequest): Promise { const processedQuery = this.processQuery(query); processedQuery.tableId = getTemplateSrv().replace(processedQuery.tableId, scopedVars); const metadata = await this.getTableMetadata(processedQuery.tableId); From 268d2fb92c3f785169dfab38e2ffc6352a780c6e Mon Sep 17 00:00:00 2001 From: Carson Moore Date: Wed, 1 Nov 2023 16:17:51 -0500 Subject: [PATCH 4/4] pr feedback --- .../data-frame/components/DataFrameQueryEditor.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/datasources/data-frame/components/DataFrameQueryEditor.tsx b/src/datasources/data-frame/components/DataFrameQueryEditor.tsx index 1d3faeb..b27d552 100644 --- a/src/datasources/data-frame/components/DataFrameQueryEditor.tsx +++ b/src/datasources/data-frame/components/DataFrameQueryEditor.tsx @@ -138,15 +138,14 @@ const getVariableOptions = () => { }; const tooltips = { - queryType: `Data allows you to visualize the rows of data in a table. Metadata allows you - to visualize the properties associated with a table.`, + queryType: `Specifies whether to visualize the data rows or properties associated with a table.`, columns: `Specifies the columns to include in the response data.`, decimation: `Specifies the method used to decimate the data.`, - filterNulls: `Filters out null and NaN values before decimating the data.`, + filterNulls: `Specifies whether to filter out null and NaN values before decimating the data.`, - useTimeRange: `Queries only for data within the dashboard time range if the table index is a - timestamp. Enable when interacting with your data on a graph.`, + useTimeRange: `Specifies whether to query only for data within the dashboard time range if the + table index is a timestamp. Enable when interacting with your data on a graph.`, };