diff --git a/src/datasources/tag/TagDataSource.test.ts b/src/datasources/tag/TagDataSource.test.ts index 2ae875a..07279e2 100644 --- a/src/datasources/tag/TagDataSource.test.ts +++ b/src/datasources/tag/TagDataSource.test.ts @@ -1,6 +1,6 @@ import { MockProxy } from 'jest-mock-extended'; import { TagDataSource } from './TagDataSource'; -import { createQueryRequest, setupDataSource, createFetchError } from 'test/fixtures'; +import { setupDataSource, createFetchError, getQueryBuilder, defaultQueryOptions } from 'test/fixtures'; import { BackendSrv, TemplateSrv } from '@grafana/runtime'; import { TagHistoryResponse, TagQuery, TagQueryType, TagsWithValues } from './types'; @@ -10,6 +10,8 @@ beforeEach(() => { [ds, backendSrv, templateSrv] = setupDataSource(TagDataSource); }); +const buildQuery = getQueryBuilder()({ type: TagQueryType.Current, workspace: '' }); + describe('testDatasource', () => { test('returns success', async () => { backendSrv.get.calledWith('/nitag/v2/tags-count').mockResolvedValue(25); @@ -32,7 +34,7 @@ describe('queries', () => { .calledWith('/nitag/v2/query-tags-with-values', expect.objectContaining({ filter: 'path = "my.tag"' })) .mockResolvedValue(createQueryTagsResponse('my.tag', '3.14')); - const result = await ds.query(createQueryRequest({ type: TagQueryType.Current, path: 'my.tag' })); + const result = await ds.query(buildQuery({ path: 'my.tag' })); expect(result.data).toEqual([ { @@ -46,7 +48,7 @@ describe('queries', () => { test('applies query defaults when missing fields', async () => { backendSrv.post.mockResolvedValue(createQueryTagsResponse('my.tag', '3.14')); - const result = await ds.query(createQueryRequest({ path: 'my.tag' } as TagQuery)); + const result = await ds.query({ ...defaultQueryOptions, targets: [{ path: 'my.tag'} as TagQuery]}); expect(result.data[0]).toHaveProperty('fields', [{ name: 'value', values: ['3.14'] }]); }); @@ -54,7 +56,7 @@ describe('queries', () => { test('uses displayName property', async () => { backendSrv.post.mockResolvedValue(createQueryTagsResponse('my.tag', '3.14', 'My cool tag')); - const result = await ds.query(createQueryRequest({ type: TagQueryType.Current, path: 'my.tag' })); + const result = await ds.query(buildQuery({ path: 'my.tag' })); expect(result.data[0]).toEqual(expect.objectContaining({ name: 'My cool tag' })); }); @@ -64,13 +66,7 @@ describe('queries', () => { .mockResolvedValueOnce(createQueryTagsResponse('my.tag1', '3.14')) .mockResolvedValueOnce(createQueryTagsResponse('my.tag2', 'foo')); - const result = await ds.query( - createQueryRequest( - { type: TagQueryType.Current, path: 'my.tag1' }, - { type: TagQueryType.Current, path: '' }, - { type: TagQueryType.Current, path: 'my.tag2' } - ) - ); + const result = await ds.query(buildQuery({ path: 'my.tag1' }, { path: '' }, { path: 'my.tag2' })); expect(backendSrv.post.mock.calls[0][1]).toHaveProperty('filter', 'path = "my.tag1"'); expect(backendSrv.post.mock.calls[1][1]).toHaveProperty('filter', 'path = "my.tag2"'); @@ -91,13 +87,13 @@ describe('queries', () => { test('throw when no tags matched', async () => { backendSrv.post.mockResolvedValue({ tagsWithValues: [] }); - await expect(ds.query(createQueryRequest({ type: TagQueryType.Current, path: 'my.tag' }))).rejects.toThrow( + await expect(ds.query(buildQuery({ path: 'my.tag' }))).rejects.toThrow( 'my.tag' ); }); test('numeric tag history', async () => { - const queryRequest = createQueryRequest({ type: TagQueryType.History, path: 'my.tag' }); + const queryRequest = buildQuery({ type: TagQueryType.History, path: 'my.tag' }); backendSrv.post .calledWith('/nitag/v2/query-tags-with-values', expect.objectContaining({ filter: 'path = "my.tag"' })) @@ -144,7 +140,7 @@ describe('queries', () => { ]) ); - const result = await ds.query(createQueryRequest({ type: TagQueryType.History, path: 'my.tag' })); + const result = await ds.query(buildQuery({ type: TagQueryType.History, path: 'my.tag' })); expect(result.data).toEqual([ { @@ -159,7 +155,7 @@ describe('queries', () => { }); test('decimation parameter does not go above 1000', async () => { - const queryRequest = createQueryRequest({ type: TagQueryType.History, path: 'my.tag' }); + const queryRequest = buildQuery({ type: TagQueryType.History, path: 'my.tag' }); queryRequest.maxDataPoints = 1500; backendSrv.post.mockResolvedValueOnce(createQueryTagsResponse('my.tag', '3')); @@ -182,18 +178,28 @@ describe('queries', () => { .calledWith('/nitag/v2/query-tags-with-values', expect.objectContaining({ filter: 'path = "my.tag"' })) .mockResolvedValue(createQueryTagsResponse('my.tag', '3.14')); - const result = await ds.query(createQueryRequest({ type: TagQueryType.Current, path: '$my_variable' })); + const result = await ds.query(buildQuery({ type: TagQueryType.Current, path: '$my_variable' })); expect(result.data[0]).toHaveProperty('name', 'my.tag'); }); + + test('filters by workspace if provided', async () => { + backendSrv.post.mockResolvedValueOnce(createQueryTagsResponse('my.tag', '3.14', undefined, '2')); + backendSrv.post.mockResolvedValueOnce(createTagHistoryResponse('my.tag', 'DOUBLE', [])); + + await ds.query(buildQuery({ type: TagQueryType.History, path: 'my.tag', workspace: '2' })); + + expect(backendSrv.post.mock.calls[0][1]).toHaveProperty('filter', 'path = "my.tag" && workspace = "2"'); + expect(backendSrv.post.mock.calls[1][1]).toHaveProperty('workspace', '2'); + }); }); -function createQueryTagsResponse(path: string, value: string, displayName?: string): TagsWithValues { +function createQueryTagsResponse(path: string, value: string, displayName?: string, workspace_id = '1'): TagsWithValues { return { tagsWithValues: [ { current: { value: { value } }, - tag: { path, properties: { displayName }, workspace_id: '1' }, + tag: { path, properties: { displayName }, workspace_id }, }, ], }; diff --git a/src/datasources/tag/TagDataSource.ts b/src/datasources/tag/TagDataSource.ts index fb84867..8640e85 100644 --- a/src/datasources/tag/TagDataSource.ts +++ b/src/datasources/tag/TagDataSource.ts @@ -19,10 +19,15 @@ export class TagDataSource extends DataSourceBase { defaultQuery = { type: TagQueryType.Current, path: '', + workspace: '', }; async runQuery(query: TagQuery, { range, maxDataPoints, scopedVars }: DataQueryRequest): Promise { - const { tag, current } = await this.getLastUpdatedTag(this.templateSrv.replace(query.path, scopedVars)); + const { tag, current } = await this.getLastUpdatedTag( + this.templateSrv.replace(query.path, scopedVars), + query.workspace + ); + const name = tag.properties.displayName ?? tag.path; if (query.type === TagQueryType.Current) { @@ -44,9 +49,14 @@ export class TagDataSource extends DataSourceBase { }; } - private async getLastUpdatedTag(path: string) { + private async getLastUpdatedTag(path: string, workspace: string) { + let filter = `path = "${path}"`; + if (workspace) { + filter += ` && workspace = "${workspace}"`; + } + const response = await this.backendSrv.post(this.tagUrl + '/query-tags-with-values', { - filter: `path = "${path}"`, + filter, take: 1, orderBy: 'TIMESTAMP', descending: true, diff --git a/src/datasources/tag/components/TagQueryEditor.test.tsx b/src/datasources/tag/components/TagQueryEditor.test.tsx index bd47971..93eb096 100644 --- a/src/datasources/tag/components/TagQueryEditor.test.tsx +++ b/src/datasources/tag/components/TagQueryEditor.test.tsx @@ -10,19 +10,21 @@ const render = setupRenderer(TagQueryEditor, TagDataSource); it('renders with query defaults', async () => { render({} as TagQuery); - expect(screen.getByLabelText('Tag path')).not.toHaveValue(); expect(screen.getByRole('radio', { name: 'Current' })).toBeChecked(); + expect(screen.getByLabelText('Tag path')).not.toHaveValue(); + expect(screen.getByLabelText('Workspace')).not.toHaveValue(); }); it('renders with saved query values', async () => { - render({ type: TagQueryType.History, path: 'my.tag' }); + render({ type: TagQueryType.History, path: 'my.tag', workspace: '1' }); - expect(screen.getByLabelText('Tag path')).toHaveValue('my.tag'); expect(screen.getByRole('radio', { name: 'History' })).toBeChecked(); + expect(screen.getByLabelText('Tag path')).toHaveValue('my.tag'); + expect(screen.getByLabelText('Workspace')).toHaveValue('1'); }); it('updates query when user types new path', async () => { - const [onChange] = render({ type: TagQueryType.Current, path: '' }); + const [onChange] = render({ type: TagQueryType.Current, path: '', workspace: '' }); await userEvent.type(screen.getByLabelText('Tag path'), 'my.tag{enter}'); @@ -30,9 +32,17 @@ it('updates query when user types new path', async () => { }); it('updates query when user selects new type', async () => { - const [onChange] = render({ type: TagQueryType.Current, path: '' }); + const [onChange] = render({ type: TagQueryType.Current, path: '', workspace: '' }); await userEvent.click(screen.getByRole('radio', { name: 'History' })); expect(onChange).toBeCalledWith(expect.objectContaining({ type: TagQueryType.History })); }); + +it('updates query when user types new workspace', async () => { + const [onChange] = render({ type: TagQueryType.Current, path: '', workspace: '' }); + + await userEvent.type(screen.getByLabelText('Workspace'), '1234{enter}'); + + expect(onChange).toBeCalledWith(expect.objectContaining({ workspace: '1234' })); +}); diff --git a/src/datasources/tag/components/TagQueryEditor.tsx b/src/datasources/tag/components/TagQueryEditor.tsx index f8212f2..4712d27 100644 --- a/src/datasources/tag/components/TagQueryEditor.tsx +++ b/src/datasources/tag/components/TagQueryEditor.tsx @@ -21,14 +21,27 @@ export function TagQueryEditor({ query, onChange, onRunQuery, datasource }: Prop onRunQuery(); }; + const onWorkspaceChange = (event: FormEvent) => { + onChange({ ...query, workspace: event.currentTarget.value }); + onRunQuery(); + }; + return ( <> - + - + + + + ); } diff --git a/src/datasources/tag/types.ts b/src/datasources/tag/types.ts index 6465d2b..66a8d77 100644 --- a/src/datasources/tag/types.ts +++ b/src/datasources/tag/types.ts @@ -8,13 +8,9 @@ export enum TagQueryType { export interface TagQuery extends DataQuery { type: TagQueryType; path: string; + workspace: string; } -export const defaultTagQuery: Omit = { - type: TagQueryType.Current, - path: '', -}; - export interface TagWithValue { current: { value: { value: string } }; tag: { diff --git a/src/test/fixtures.ts b/src/test/fixtures.ts index 3b00f75..b7f613e 100644 --- a/src/test/fixtures.ts +++ b/src/test/fixtures.ts @@ -49,19 +49,23 @@ export function createFetchError(status: number): FetchError { return mock({ status }); } -export function createQueryRequest( - ...targets: Array> -): DataQueryRequest { - return { - targets: targets.map((t, ix) => ({ ...t, refId: 'ABCDE'[ix] } as TQuery)), - requestId: '', - interval: '', - intervalMs: 0, - range: { from: dateTime().subtract(1, 'h'), to: dateTime(), raw: { from: 'now-6h', to: 'now' } }, - scopedVars: {}, - timezone: 'browser', - app: 'panel-editor', - startTime: 0, - maxDataPoints: 300, +export const defaultQueryOptions: Omit = { + requestId: '', + interval: '', + intervalMs: 0, + range: { from: dateTime().subtract(1, 'h'), to: dateTime(), raw: { from: 'now-6h', to: 'now' } }, + scopedVars: {}, + timezone: 'browser', + app: 'panel-editor', + startTime: 0, + maxDataPoints: 300, +}; + +export function getQueryBuilder() { + return (defaults: Pick) => { + return (...targets: Array & Partial>): DataQueryRequest => ({ + targets: targets.map((t, ix) => ({ ...defaults, ...t, refId: 'ABCDE'[ix] } as TQuery)), + ...defaultQueryOptions, + }); }; }