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(tag): handle multiple tags #57

Closed
Closed
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
49 changes: 39 additions & 10 deletions src/core/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,44 @@
import { enumToOptions } from "./utils";
import { enumToOptions, expandMultipleValueVariableAfterReplace } from "./utils";

test('enumToOptions', () => {
enum fakeStringEnum {
Label1 = 'Value1',
Label2 = 'Value2'
};
enum fakeStringEnum {
Label1 = 'Value1',
Label2 = 'Value2'
}

const result = enumToOptions(fakeStringEnum);
const result = enumToOptions(fakeStringEnum);

expect(result).toEqual([
{ label: 'Label1', value: 'Value1' },
{ label: 'Label2', value: 'Value2' }
]);
expect(result).toEqual([
{ label: 'Label1', value: 'Value1' },
{ label: 'Label2', value: 'Value2' }
]);
});

describe('expandMultipleValueVariableAfterReplace', () => {
test('basicString', () => {
expect(expandMultipleValueVariableAfterReplace('a{{b,c,d}}e')).toEqual(['abe', 'ace', 'ade']);
})

test('doublePatternString', () => {
expect(expandMultipleValueVariableAfterReplace('{{a,b}}-{{c,d}}')).toEqual(['a-c', 'a-d', 'b-c', 'b-d']);
})
test('doublePatternWithoutSeparatorString', () => {
expect(expandMultipleValueVariableAfterReplace('{{a,b}}{{c,d}}')).toEqual(['ac', 'ad', 'bc', 'bd']);
})
test('plainString', () => {
expect(expandMultipleValueVariableAfterReplace('plainString')).toEqual(['plainString']);
})
test('ignoreEmptyElementsInBracesString', () => {
expect(expandMultipleValueVariableAfterReplace('a{{b,c,,d,e,,f,}}g')).toEqual(['abg', 'acg', 'adg', 'aeg', 'afg']);
})
test('nestedString', () => {
expect(() => {
expandMultipleValueVariableAfterReplace('a{{b,{{c}}},e')
}).toThrow('Nested braces are not allowed');
})
test('unmatchedBracesString', () => {
expect(() => {
expandMultipleValueVariableAfterReplace('error{{test')
}).toThrow('Unmatched braces');
})
})
38 changes: 38 additions & 0 deletions src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,41 @@ export function replaceVariables(values: string[], templateSrv: TemplateSrv) {
// Dedupe and flatten
return [...new Set(replaced.flat())];
}

export const expandMultipleValueVariableAfterReplace = (input: string): string[] => {
// Helper function to recursively generate combinations
const generateCombinations = (text: string, start = 0): string[] => {
// Find the next pattern in the string
const open = text.indexOf('{{', start);
if (open === -1) {
// No more patterns, return the current string
return [text];
}
const close = text.indexOf('}}', open);
if (close === -1) {
throw new Error('Unmatched braces');
}

// Check for nested braces
const nestedOpen = text.indexOf('{{', open + 1);
if (nestedOpen !== -1 && nestedOpen < close) {
throw new Error('Nested braces are not allowed');
}

// Extract the options within the braces and generate combinations
const options = text.substring(open + 2, close).split(',').filter((option) => option !== '');
const prefix = text.substring(0, open);
const suffix = text.substring(close + 2);

let combinations: string[] = [];
for (const option of options) {
// For each option, replace the pattern with the option and generate further combinations
const newCombinations = generateCombinations(prefix + option + suffix);
combinations = combinations.concat(newCombinations);
}

return combinations;
};

return generateCombinations(input);
};
56 changes: 40 additions & 16 deletions src/datasources/tag/TagDataSource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
setupDataSource,
} from 'test/fixtures';
import { TagDataSource } from './TagDataSource';
import { TagQuery, TagQueryType, TagWithValue } from './types';
import { TagQuery, TagQueryType, TagWithValue, TagDataType } from './types';

let ds: TagDataSource, backendSrv: MockProxy<BackendSrv>, templateSrv: MockProxy<TemplateSrv>;

Expand Down Expand Up @@ -46,11 +46,9 @@ describe('testDatasource', () => {
describe('queries', () => {
test('tag current value', async () => {
backendSrv.fetch
.calledWith(requestMatching({ url: '/nitag/v2/query-tags-with-values', data: { filter: 'path = "my.tag"' } }))
.calledWith(requestMatching({ url: '/nitag/v2/query-tags-with-values', data: { filter: '(path = "my.tag")' } }))
.mockReturnValue(createQueryTagsResponse());

const result = await ds.query(buildQuery({ path: 'my.tag' }));

expect(result.data).toMatchSnapshot();
});

Expand Down Expand Up @@ -88,10 +86,10 @@ describe('queries', () => {

test('multiple targets - skips invalid queries', async () => {
backendSrv.fetch
.calledWith(requestMatching({ data: { filter: 'path = "my.tag1"' } }))
.calledWith(requestMatching({ data: { filter: '(path = \"my.tag1\")' } }))
.mockReturnValue(createQueryTagsResponse({ path: 'my.tag1' }));
backendSrv.fetch
.calledWith(requestMatching({ data: { filter: 'path = "my.tag2"' } }))
.calledWith(requestMatching({ data: { filter: '(path = \"my.tag2\")' } }))
.mockReturnValue(createQueryTagsResponse({ path: 'my.tag2' }, { value: { value: '41.3' } }));

const result = await ds.query(buildQuery({ path: 'my.tag1' }, { path: '' }, { path: 'my.tag2' }));
Expand All @@ -101,12 +99,21 @@ describe('queries', () => {

test('current value for all data types', async () => {
backendSrv.fetch
.mockReturnValueOnce(createQueryTagsResponse({ type: 'INT', path: 'tag1' }, { value: { value: '3' } }))
.mockReturnValueOnce(createQueryTagsResponse({ type: 'DOUBLE', path: 'tag2' }, { value: { value: '3.3' } }))
.mockReturnValueOnce(createQueryTagsResponse({ type: 'STRING', path: 'tag3' }, { value: { value: 'foo' } }))
.mockReturnValueOnce(createQueryTagsResponse({ type: 'BOOLEAN', path: 'tag4' }, { value: { value: 'True' } }))
.mockReturnValueOnce(createQueryTagsResponse({ type: TagDataType.INT, path: 'tag1' }, { value: { value: '3' } }))
.mockReturnValueOnce(createQueryTagsResponse({
type: TagDataType.DOUBLE,
path: 'tag2'
}, { value: { value: '3.3' } }))
.mockReturnValueOnce(createQueryTagsResponse({
type: TagDataType.STRING,
path: 'tag3'
}, { value: { value: 'foo' } }))
.mockReturnValueOnce(createQueryTagsResponse({
type: TagDataType.BOOLEAN,
path: 'tag4'
}, { value: { value: 'True' } }))
.mockReturnValueOnce(
createQueryTagsResponse({ type: 'U_INT64', path: 'tag5' }, { value: { value: '2147483648' } })
createQueryTagsResponse({ type: TagDataType.U_INT64, path: 'tag5' }, { value: { value: '2147483648' } })
);

const result = await ds.query(
Expand All @@ -119,14 +126,14 @@ describe('queries', () => {
test('throw when no tags matched', async () => {
backendSrv.fetch.mockReturnValue(createFetchResponse({ tagsWithValues: [] }));

await expect(ds.query(buildQuery({ path: 'my.tag' }))).rejects.toThrow("No tags matched the path 'my.tag'");
await expect(ds.query(buildQuery({ path: 'my.tag' }))).rejects.toThrow("No tags matched the path 'path = \"my.tag\"'");
});

test('numeric tag history', async () => {
const queryRequest = buildQuery({ type: TagQueryType.History, path: 'my.tag' });

backendSrv.fetch
.calledWith(requestMatching({ url: '/nitag/v2/query-tags-with-values', data: { filter: 'path = "my.tag"' } }))
.calledWith(requestMatching({ url: '/nitag/v2/query-tags-with-values', data: { filter: '(path = "my.tag")' } }))
.mockReturnValue(createQueryTagsResponse());

backendSrv.fetch
Expand Down Expand Up @@ -188,8 +195,10 @@ describe('queries', () => {

test('replaces tag path with variable', async () => {
templateSrv.replace.calledWith('$my_variable').mockReturnValue('my.tag');
templateSrv.containsTemplate.calledWith('$my_variable').mockReturnValue(true);

backendSrv.fetch
.calledWith(requestMatching({ url: '/nitag/v2/query-tags-with-values', data: { filter: 'path = "my.tag"' } }))
.calledWith(requestMatching({ url: '/nitag/v2/query-tags-with-values', data: { filter: '(path = "my.tag")' } }))
.mockReturnValue(createQueryTagsResponse());

const result = await ds.query(buildQuery({ type: TagQueryType.Current, path: '$my_variable' }));
Expand All @@ -203,7 +212,7 @@ describe('queries', () => {

await ds.query(buildQuery({ type: TagQueryType.History, path: 'my.tag', workspace: '2' }));

expect(backendSrv.fetch.mock.calls[0][0].data).toHaveProperty('filter', 'path = "my.tag" && workspace = "2"');
expect(backendSrv.fetch.mock.calls[0][0].data).toHaveProperty('filter', '(path = "my.tag") AND workspace = "2"');
expect(backendSrv.fetch.mock.calls[1][0].data).toHaveProperty('workspace', '2');
});

Expand Down Expand Up @@ -237,14 +246,29 @@ describe('queries', () => {
expect(result.data).toMatchSnapshot();
});

test('attempts to replace variables in history query', async () => {
test('attempts to replace workspace variables in history query', async () => {
const workspaceVariable = '$workspace';
backendSrv.fetch.mockReturnValueOnce(createQueryTagsResponse());
templateSrv.replace.calledWith(workspaceVariable).mockReturnValue('1');

await ds.query(buildQuery({ path: 'my.tag', workspace: workspaceVariable }));

expect(templateSrv.replace).toHaveBeenCalledTimes(1);
expect(templateSrv.replace.mock.calls[0][0]).toBe(workspaceVariable);
});

test('attempts to replace tag and workspace variables in history query', async () => {
backendSrv.fetch.mockReturnValueOnce(createQueryTagsResponse());
const tagVariable = '$my_variable';
const workspaceVariable = '$workspace';
templateSrv.replace.calledWith(workspaceVariable).mockReturnValue('1');
templateSrv.containsTemplate.calledWith(tagVariable).mockReturnValue(true);
templateSrv.replace.calledWith(tagVariable).mockReturnValue('my.tag');

await ds.query(buildQuery({ path: tagVariable, workspace: workspaceVariable }));

expect(templateSrv.replace).toHaveBeenCalledTimes(2);
expect(templateSrv.replace.mock.calls[0][0]).toBe(tagVariable);
expect(templateSrv.replace.mock.calls[1][0]).toBe(workspaceVariable);
});

Expand Down
Loading
Loading