Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dataframe): visualize table properties #49

Merged
merged 4 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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