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(products): Add query builder to the Products Datasource #116

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
96c2b3e
Added basic control in the query editor
richie-ni Jan 9, 2025
defd66c
refactor(ProductQueryEditor): streamline query change handling and im…
richie-ni Jan 13, 2025
54e7832
add test cases for datasource
richie-ni Jan 15, 2025
95aee82
add missing newlines at the end of test files and update README descr…
richie-ni Jan 15, 2025
8ff813e
refactor(ProductQueryEditor.test): simplify tests using setupRenderer…
richie-ni Jan 15, 2025
cdac3ea
feat(ProductQueryEditor): add placeholders and enhance test coverage …
richie-ni Jan 15, 2025
49e0b08
fix lint error
richie-ni Jan 15, 2025
c2eb22c
fix: renamed datsaouce to products
richie-ni Jan 15, 2025
4fde1b1
feat(ProductsQueryEditor): add queryBy parameter and workspace handli…
richie-ni Jan 15, 2025
4a2c7d0
updated the alignments
richie-ni Jan 16, 2025
39bd65d
added properties to the query builder
richie-ni Jan 16, 2025
d06b373
resimplify method bindings and improve readability
richie-ni Jan 17, 2025
d342aff
added all fields to query builder
richie-ni Jan 17, 2025
72e4e89
enhance error handling in queryProducts and ensure proper field selec…
richie-ni Jan 17, 2025
4f44d67
Merge branch 'users/richie/grafana/products-datasource' into users/ri…
richie-ni Jan 17, 2025
9df0d34
refactor: standardize PropertyFieldKeyValuePair naming and improve qu…
richie-ni Jan 20, 2025
85cabae
Merge branch 'main' of https://github.com/ni/systemlink-grafana-plugi…
richie-ni Jan 20, 2025
3097c63
test: enhance ProductsDataSource tests with error handling and variab…
richie-ni Jan 20, 2025
3625b89
addressed PR comments
richie-ni Jan 20, 2025
b40ce8f
resolved comments
richie-ni Jan 21, 2025
c2e19a3
removed the snapshots
richie-ni Jan 21, 2025
6371242
added new snapshots
richie-ni Jan 21, 2025
d4f3c53
refactor: rename KeyValueOperationTemplate import and remove unused k…
richie-ni Jan 21, 2025
524581e
Merge branch 'users/richie/grafana/products-datasource' into users/ri…
richie-ni Jan 21, 2025
b91c079
refactor: simplify query building and update test snapshots
richie-ni Jan 21, 2025
2c1eb58
Merge branch 'users/richie/grafana/products-datasource' into users/ri…
richie-ni Jan 21, 2025
7bdef54
test: add queryProductValues tests and improve error handling
richie-ni Jan 21, 2025
0419b0c
refactor: enhance ProductsQueryEditor layout and add styling
richie-ni Jan 21, 2025
e827f16
fix: add newline at end of file in ProductsDataSource.test.ts
richie-ni Jan 21, 2025
4e189ca
feat: add STARTS_WITH and ENDS_WITH operations to ProductsQueryBuilder
richie-ni Jan 21, 2025
ab1c4c7
feat: enable noMultiValueWrap in ProductsQueryEditor component
richie-ni Jan 22, 2025
570f6c9
Merge branch 'users/richie/grafana/products-datasource' into users/ri…
richie-ni Jan 22, 2025
5b1ee8d
feat: add numerical comparison operations to custom query builder
richie-ni Jan 22, 2025
124071a
rename and move keyvalueOperationUtils
richie-ni Jan 23, 2025
fc634ef
fixed lint error
richie-ni Jan 23, 2025
420edd7
refactor: simplify type casting in keyValueOperationUtils
richie-ni Jan 23, 2025
441263e
Merge branch 'main' into users/richie/grafana/products-datsource/add-…
richie-ni Jan 24, 2025
33ad95d
feat(products): add variable query editor and enhance ProductsDataSou…
richie-ni Jan 27, 2025
bb69a48
Revert "feat(products): add variable query editor and enhance Product…
richie-ni Jan 27, 2025
ed83a0b
refactor(products): clean up code structure and improve query handlin…
richie-ni Jan 29, 2025
6141fd0
enhance alignment - ProductsQueryBuilder tests
richie-ni Jan 29, 2025
3482976
resolved comments
richie-ni Jan 30, 2025
9285828
fix indentation and formatting
richie-ni Jan 30, 2025
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
179 changes: 153 additions & 26 deletions src/core/query-builder.constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { editorTemplate, handleNumberValue, handleStringValue, keyValueExpressionBuilderCallback, numericKeyValueExpressionReaderCallback, stringKeyValueExpressionReaderCallback, valueTemplate } from "datasources/products/components/query-builder/utils/keyValueOperationUtils";
import { QueryBuilderCustomOperation } from "smart-webcomponents-react";

export const queryBuilderMessages = {
Expand Down Expand Up @@ -40,6 +41,32 @@ export const queryBuilderMessages = {
},
};

export enum FilterOperations {
KeyValueMatch = 'key_value_matches',
KeyValueDoesNotMatch = 'key_value_not_matches',
KeyValueContains = 'key_value_contains',
KeyValueDoesNotContains = 'key_value_not_contains',
KeyValueIsGreaterThan = 'key_value_>',
KeyValueIsGreaterThanOrEqual = 'key_value_>=',
KeyValueIsLessThan = 'key_value_<',
KeyValueIsLessThanOrEqual = 'key_value_<=',
KeyValueIsNumericallyEqual = 'key_value_===',
KeyValueIsNumericallyNotEqual = 'key_value_!==',
}

export enum FilterExpressions {
KeyValueMatches = '{0}["{1}"] = "{2}"',
KeyValueNotMatches = '{0}["{1}"] != "{2}"',
KeyValueContains = '{0}["{1}"].Contains("{2}")',
KeyValueNotContains = '!{0}["{1}"].Contains("{2}")',
KeyValueIsGreaterThan = 'SafeConvert.ToDecimal({0}["{1}"]) > {2}',
KeyValueIsGreaterThanOrEqual = 'SafeConvert.ToDecimal({0}["{1}"]) >= {2}',
KeyValueIsLessThan = 'SafeConvert.ToDecimal({0}["{1}"]) < {2}',
KeyValueIsLessThanOrEqual = 'SafeConvert.ToDecimal({0}["{1}"]) <= {2}',
KeyValueIsNumericallyEqual = 'SafeConvert.ToDecimal({0}["{1}"]) = {2}',
KeyValueIsNumericallyNotEqual = 'SafeConvert.ToDecimal({0}["{1}"]) != {2}',
}

export const QueryBuilderOperations = {
EQUALS: {
label: 'Equals',
Expand Down Expand Up @@ -167,31 +194,131 @@ export const QueryBuilderOperations = {
expressionTemplate: '!string.IsNullOrEmpty(properties["{0}"])',
hideValue: true,
},
}
KEY_VALUE_MATCH: {
label: `matches`,
name: FilterOperations.KeyValueMatch,
expressionTemplate: FilterExpressions.KeyValueMatches,
editorTemplate: editorTemplate,
valueTemplate: valueTemplate,
handleValue: handleStringValue,
expressionBuilderCallback: keyValueExpressionBuilderCallback,
expressionReaderCallback: stringKeyValueExpressionReaderCallback,
},
KEY_VALUE_DOES_NOT_MATCH: {
label: `does not match`,
name: FilterOperations.KeyValueDoesNotMatch,
expressionTemplate: FilterExpressions.KeyValueNotMatches,
editorTemplate: editorTemplate,
valueTemplate: valueTemplate,
handleValue: handleStringValue,
expressionBuilderCallback: keyValueExpressionBuilderCallback,
expressionReaderCallback: stringKeyValueExpressionReaderCallback,
},
KEY_VALUE_DOES_NOT_CONTAINS: {
label: `does not contain`,
name: FilterOperations.KeyValueDoesNotContains,
expressionTemplate: FilterExpressions.KeyValueNotContains,
editorTemplate: editorTemplate,
valueTemplate: valueTemplate,
handleValue: handleStringValue,
expressionBuilderCallback: keyValueExpressionBuilderCallback,
expressionReaderCallback: stringKeyValueExpressionReaderCallback,
},
KEY_VALUE_CONTAINS: {
label: `contains`,
name: FilterOperations.KeyValueContains,
expressionTemplate: FilterExpressions.KeyValueContains,
editorTemplate: editorTemplate,
valueTemplate: valueTemplate,
handleValue: handleStringValue,
expressionBuilderCallback: keyValueExpressionBuilderCallback,
expressionReaderCallback: stringKeyValueExpressionReaderCallback,
},
KEY_VALUE_IS_GREATER_THAN: {
label: `> (numeric)`,
name: FilterOperations.KeyValueIsGreaterThan,
expressionTemplate: FilterExpressions.KeyValueIsGreaterThan,
editorTemplate: editorTemplate,
valueTemplate: valueTemplate,
handleValue: handleNumberValue,
expressionBuilderCallback: keyValueExpressionBuilderCallback,
expressionReaderCallback: numericKeyValueExpressionReaderCallback,
},
KEY_VALUE_IS_GREATER_THAN_OR_EQUAL: {
label: `≥ (numeric)`,
name: FilterOperations.KeyValueIsGreaterThanOrEqual,
expressionTemplate: FilterExpressions.KeyValueIsGreaterThanOrEqual,
editorTemplate: editorTemplate,
valueTemplate: valueTemplate,
handleValue: handleNumberValue,
expressionBuilderCallback: keyValueExpressionBuilderCallback,
expressionReaderCallback: numericKeyValueExpressionReaderCallback,
},
KEY_VALUE_IS_LESS_THAN: {
label: `< (numeric)`,
name: FilterOperations.KeyValueIsLessThan,
expressionTemplate: FilterExpressions.KeyValueIsLessThan,
editorTemplate: editorTemplate,
valueTemplate: valueTemplate,
handleValue: handleNumberValue,
expressionBuilderCallback: keyValueExpressionBuilderCallback,
expressionReaderCallback: numericKeyValueExpressionReaderCallback,
},
KEY_VALUE_IS_LESS_THAN_OR_EQUAL: {
label: `≤ (numeric)`,
name: FilterOperations.KeyValueIsLessThanOrEqual,
expressionTemplate: FilterExpressions.KeyValueIsLessThanOrEqual,
editorTemplate: editorTemplate,
valueTemplate: valueTemplate,
handleValue: handleNumberValue,
expressionBuilderCallback: keyValueExpressionBuilderCallback,
expressionReaderCallback: numericKeyValueExpressionReaderCallback,
},
KEY_VALUE_IS_NUMERICAL_EQUAL: {
label: `= (numeric)`,
name: FilterOperations.KeyValueIsNumericallyEqual,
expressionTemplate: FilterExpressions.KeyValueIsNumericallyEqual,
editorTemplate: editorTemplate,
valueTemplate: valueTemplate,
handleValue: handleNumberValue,
expressionBuilderCallback: keyValueExpressionBuilderCallback,
expressionReaderCallback: numericKeyValueExpressionReaderCallback,
},
KEY_VALUE_IS_NUMERICAL_NOT_EQUAL: {
label: `≠ (numeric)`,
name: FilterOperations.KeyValueIsNumericallyNotEqual,
expressionTemplate: FilterExpressions.KeyValueIsNumericallyNotEqual,
editorTemplate: editorTemplate,
valueTemplate: valueTemplate,
handleValue: handleNumberValue,
expressionBuilderCallback: keyValueExpressionBuilderCallback,
expressionReaderCallback: numericKeyValueExpressionReaderCallback,
}
};

export const customOperations: QueryBuilderCustomOperation[] = [
QueryBuilderOperations.EQUALS,
QueryBuilderOperations.DOES_NOT_EQUAL,
QueryBuilderOperations.STARTS_WITH,
QueryBuilderOperations.ENDS_WITH,
QueryBuilderOperations.CONTAINS,
QueryBuilderOperations.DOES_NOT_CONTAIN,
QueryBuilderOperations.IS_BLANK,
QueryBuilderOperations.IS_NOT_BLANK,
QueryBuilderOperations.GREATER_THAN,
QueryBuilderOperations.GREATER_THAN_OR_EQUAL_TO,
QueryBuilderOperations.LESS_THAN,
QueryBuilderOperations.LESS_THAN_OR_EQUAL_TO,
QueryBuilderOperations.LIST_EQUALS,
QueryBuilderOperations.LIST_DOES_NOT_EQUAL,
QueryBuilderOperations.LIST_CONTAINS,
QueryBuilderOperations.LIST_DOES_NOT_CONTAIN,
QueryBuilderOperations.PROPERTY_EQUALS,
QueryBuilderOperations.PROPERTY_DOES_NOT_EQUAL,
QueryBuilderOperations.PROPERTY_STARTS_WITH,
QueryBuilderOperations.PROPERTY_ENDS_WITH,
QueryBuilderOperations.PROPERTY_CONTAINS,
QueryBuilderOperations.PROPERTY_DOES_NOT_CONTAIN,
QueryBuilderOperations.PROPERTY_IS_BLANK,
QueryBuilderOperations.PROPERTY_IS_NOT_BLANK,
];
QueryBuilderOperations.EQUALS,
QueryBuilderOperations.DOES_NOT_EQUAL,
QueryBuilderOperations.STARTS_WITH,
QueryBuilderOperations.ENDS_WITH,
QueryBuilderOperations.CONTAINS,
QueryBuilderOperations.DOES_NOT_CONTAIN,
QueryBuilderOperations.IS_BLANK,
QueryBuilderOperations.IS_NOT_BLANK,
QueryBuilderOperations.GREATER_THAN,
QueryBuilderOperations.GREATER_THAN_OR_EQUAL_TO,
QueryBuilderOperations.LESS_THAN,
QueryBuilderOperations.LESS_THAN_OR_EQUAL_TO,
QueryBuilderOperations.LIST_EQUALS,
QueryBuilderOperations.LIST_DOES_NOT_EQUAL,
QueryBuilderOperations.LIST_CONTAINS,
QueryBuilderOperations.LIST_DOES_NOT_CONTAIN,
QueryBuilderOperations.PROPERTY_EQUALS,
QueryBuilderOperations.PROPERTY_DOES_NOT_EQUAL,
QueryBuilderOperations.PROPERTY_STARTS_WITH,
QueryBuilderOperations.PROPERTY_ENDS_WITH,
QueryBuilderOperations.PROPERTY_CONTAINS,
QueryBuilderOperations.PROPERTY_DOES_NOT_CONTAIN,
QueryBuilderOperations.PROPERTY_IS_BLANK,
QueryBuilderOperations.PROPERTY_IS_NOT_BLANK,
];
107 changes: 106 additions & 1 deletion src/datasources/products/ProductsDataSource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ProductQuery, Properties, PropertiesOptions, QueryProductResponse } fro
import { Field } from '@grafana/data';
import { createFetchError, createFetchResponse, getQueryBuilder, requestMatching, setupDataSource } from 'test/fixtures';
import { MockProxy } from 'jest-mock-extended';
import { ProductsQueryBuilderFieldNames } from './constants/ProductsQueryBuilder.constants';

const mockQueryProductResponse: QueryProductResponse = {
products: [
Expand Down Expand Up @@ -62,7 +63,7 @@ describe('queryProducts', () => {
expect(response).toMatchSnapshot();
});

test('raises an error returns API fails', async () => {
test('raises an error when API fails', async () => {
backendServer.fetch
.calledWith(requestMatching({ url: '/nitestmonitor/v2/query-products' }))
.mockReturnValue(createFetchError(400));
Expand All @@ -73,6 +74,28 @@ describe('queryProducts', () => {
});
});

describe('queryProductValues', () => {
test('returns data when there are valid queries', async () => {
backendServer.fetch
.calledWith(requestMatching({ url: '/nitestmonitor/v2/query-product-values' }))
.mockReturnValue(createFetchResponse(['value1']));

const response = await datastore.queryProductValues(ProductsQueryBuilderFieldNames.PART_NUMBER);

expect(response).toMatchSnapshot();
});

test('raises an error when API fails', async () => {
backendServer.fetch
.calledWith(requestMatching({ url: '/nitestmonitor/v2/query-product-values' }))
.mockReturnValue(createFetchError(400));

await expect(datastore.queryProductValues(ProductsQueryBuilderFieldNames.PART_NUMBER))
.rejects
.toThrow('Request to url "/nitestmonitor/v2/query-product-values" failed with status code: 400. Error message: "Error"');
});
});

describe('query', () => {
test('returns data when there are valid queries', async () => {
const query = buildQuery(
Expand All @@ -84,6 +107,7 @@ describe('query', () => {
PropertiesOptions.NAME,
PropertiesOptions.WORKSPACE
] as Properties[],
queryBy: `${ProductsQueryBuilderFieldNames.PART_NUMBER} = "123"`,
orderBy: PropertiesOptions.ID,
descending: false,
recordCount: 1
Expand Down Expand Up @@ -192,6 +216,87 @@ describe('query', () => {
{ name: 'properties', values: [''], type: 'string' },
]);
});

test('should not query product values if cache exists', async () => {
backendServer.fetch
.calledWith(requestMatching({ url: '/nitestmonitor/v2/query-product-values' }))
.mockReturnValue(createFetchResponse(['value1']));
datastore.partNumbersCache.set('partNumber', 'value1');
backendServer.fetch.mockClear();

await datastore.query(buildQuery())

expect(backendServer.fetch).not.toHaveBeenCalled();
});

test('should not query workspace values if cache exists', async () => {
backendServer.fetch
.calledWith(requestMatching({ url: '/niauth/v1/user' }))
.mockReturnValue(createFetchResponse(['workspace1']));
datastore.workspacesCache.set('workspace', { id: 'workspace1', name: 'workspace1', default: false, enabled: true });
backendServer.fetch.mockClear();

await datastore.query(buildQuery())

expect(backendServer.fetch).not.toHaveBeenCalled();
});

describe('Query builder queries', () => {
test('should transform fields with single value', async () => {
const query = buildQuery(
{
refId: 'A',
properties: [
PropertiesOptions.PART_NUMBER
] as Properties[],
orderBy: undefined,
queryBy: `${PropertiesOptions.PART_NUMBER} = '123'`
},
);

await datastore.query(query);

expect(backendServer.fetch).toHaveBeenCalledWith(
expect.objectContaining({
data: {
descending: false,
filter: "partNumber = '123'",
orderBy: undefined,
projection: ["partNumber"],
returnCount: false, take: 1000
}
})
);
});

test('should transform fields with multiple values', async () => {
const query = buildQuery(
{
refId: 'A',
properties: [
PropertiesOptions.PART_NUMBER
] as Properties[],
orderBy: undefined,
queryBy: `${ProductsQueryBuilderFieldNames.PART_NUMBER} = "{partNumber1,partNumber2}"`
},
);

await datastore.query(query);

expect(backendServer.fetch).toHaveBeenCalledWith(
expect.objectContaining({
data: {
descending: false,
filter: "PartNumber = \"partNumber1\" || PartNumber = \"partNumber2\"",
orderBy: undefined,
projection: ["partNumber"],
returnCount: false, take: 1000
}
})
);
});

});
});

const buildQuery = getQueryBuilder<ProductQuery>()({
Expand Down
Loading