Skip to content

Commit

Permalink
feat(dataframe): visualize table properties (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
mure authored Nov 1, 2023
1 parent 5dfd5b7 commit 8006749
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 156 deletions.
11 changes: 2 additions & 9 deletions src/core/DataSourceBase.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -28,7 +21,7 @@ export abstract class DataSourceBase<TQuery extends DataQuery> extends DataSourc
}

abstract defaultQuery: Partial<TQuery> & Omit<TQuery, 'refId'>;
abstract runQuery(query: TQuery, options: DataQueryRequest): Promise<DataFrame | DataFrameDTO>;
abstract runQuery(query: TQuery, options: DataQueryRequest): Promise<DataFrameDTO>;
abstract shouldRunQuery(query: TQuery): boolean;
abstract testDatasource(): Promise<TestingStatus>;

Expand Down
117 changes: 83 additions & 34 deletions src/datasources/data-frame/DataFrameDataSource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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: '1', columns: ['float'] },
]);

const response = await ds.query(query);
Expand All @@ -65,21 +65,21 @@ it('should convert columns to Grafana fields', async () => {
const query = buildQuery([
{
refId: 'A',
tableId: '_',
type: DataFrameQueryType.Data,
tableId: '1',
columns: ['int', 'float', 'string', 'time', 'bool', 'Value'],
},
]);

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' } },
]);
});
Expand All @@ -88,7 +88,8 @@ it('should automatically apply time filters when index column is a timestamp', a
const query = buildQuery([
{
refId: 'A',
tableId: '_',
type: DataFrameQueryType.Data,
tableId: '1',
columns: ['time'],
applyTimeFilters: true,
},
Expand All @@ -115,7 +116,8 @@ it('should apply null and NaN filters', async () => {
const query = buildQuery([
{
refId: 'A',
tableId: '_',
type: DataFrameQueryType.Data,
tableId: '1',
columns: ['int', 'float', 'string'],
filterNulls: true,
},
Expand All @@ -140,7 +142,8 @@ it('should provide decimation parameters correctly', async () => {
const query = buildQuery([
{
refId: 'A',
tableId: '_',
type: DataFrameQueryType.Data,
tableId: '1',
columns: ['int', 'string', 'float'],
decimationMethod: 'ENTRY_EXIT',
},
Expand All @@ -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);

Expand All @@ -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());
});
Expand All @@ -193,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);

Expand All @@ -202,15 +205,40 @@ 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'] }]);
replaceMock.mockReturnValue('1');
const query = buildQuery([{ refId: 'A', type: DataFrameQueryType.Data, tableId: '$tableId', columns: ['float'] }]);
replaceMock.mockReturnValueOnce('1');

await ds.query(query);

expect(replaceMock).toHaveBeenCalledTimes(2);
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'] },
])
});

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<DataFrameQuery> => {
return {
...defaultQuery,
Expand All @@ -220,11 +248,16 @@ const buildQuery = (targets: DataFrameQuery[]): DataQueryRequest<DataFrameQuery>

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(fakeDataResponse));
return of(createFetchResponse(getFakeDataResponse(options.data.columns)));
}

throw new Error('Unexpected request');
Expand Down Expand Up @@ -255,19 +288,35 @@ 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 fakeMetadataResponseNoProperties: TableMetadata = {
columns: [{ name: 'time', dataType: 'TIMESTAMP', columnType: 'INDEX', properties: {} }],
id: '_',
properties: {},
name: 'Test Table no properties',
workspace: '_',
};

const fakeData: Record<string, string[]> = {
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<DataFrameQuery> = {
Expand Down
Loading

0 comments on commit 8006749

Please sign in to comment.