From ce7ad2ba76c195af7edf6cdacd9b3d18ae169560 Mon Sep 17 00:00:00 2001 From: Robert Aradei Date: Wed, 25 Sep 2024 18:12:50 +0300 Subject: [PATCH] feat(asset): Added calibration forecast query builder --- src/core/query-builder.constants.ts | 197 ++++++++++++++++++ src/core/query-builder.utils.test.ts | 100 +++++++++ src/core/query-builder.utils.ts | 64 ++++++ src/core/types.ts | 5 + .../AssetCalibrationDataSource.ts | 39 ++-- .../AssetCalibrationQueryBuilder.test.tsx | 36 ++++ .../AssetCalibrationQueryBuilder.tsx | 96 +++++++++ .../AssetCalibrationQueryEditor.tsx | 56 +++-- .../AssetQueryCalibrationEditor.test.tsx | 142 +++++++------ .../asset-calibration/constants.ts | 81 +++++++ src/datasources/asset-calibration/types.ts | 156 ++++++-------- src/datasources/asset-common/types.ts | 70 +++++++ src/datasources/asset/AssetDataSource.test.ts | 7 +- src/datasources/asset/AssetDataSource.ts | 3 +- src/datasources/asset/types.ts | 70 ------- src/datasources/notebook/QueryBuilder.tsx | 173 +-------------- 16 files changed, 853 insertions(+), 442 deletions(-) create mode 100644 src/core/query-builder.constants.ts create mode 100644 src/core/query-builder.utils.test.ts create mode 100644 src/core/query-builder.utils.ts create mode 100644 src/datasources/asset-calibration/components/AssetCalibrationQueryBuilder.test.tsx create mode 100644 src/datasources/asset-calibration/components/AssetCalibrationQueryBuilder.tsx create mode 100644 src/datasources/asset-calibration/constants.ts create mode 100644 src/datasources/asset-common/types.ts diff --git a/src/core/query-builder.constants.ts b/src/core/query-builder.constants.ts new file mode 100644 index 0000000..5bba89e --- /dev/null +++ b/src/core/query-builder.constants.ts @@ -0,0 +1,197 @@ +import { QueryBuilderCustomOperation } from "smart-webcomponents-react"; + +export const queryBuilderMessages = { + en: { + propertyUnknownType: "'' property is with undefined 'type' member!", + propertyInvalidValue: "Invalid '!", + propertyInvalidValueType: "Invalid '!", + elementNotInDOM: 'Element does not exist in DOM! Please, add the element to the DOM, before invoking a method.', + moduleUndefined: 'Module is undefined.', + missingReference: '.', + htmlTemplateNotSuported: ": Browser doesn't support HTMLTemplate elements.", + invalidTemplate: "' property accepts a string that must match the id of an HTMLTemplate element from the DOM.", + add: 'Add', + addCondition: 'Add Condition', + addGroup: 'Add Group', + and: 'And', + notand: 'Not And', + or: 'Or', + notor: 'Not Or', + '=': 'Equals', + '<>': 'Does not equal', + '>': 'Greater than', + '>=': 'Greater than or equal to', + '<': 'Less than', + '<=': 'Less than or equal to', + startswith: 'Starts with', + endswith: 'Ends with', + contains: 'Contains', + notcontains: 'Does not contain', + isblank: 'Is blank', + isnotblank: 'Is not blank', + wrongParentGroupIndex: "' method.", + missingFields: + ': Fields are required for proper condition\'s adding. Set "fields" source and then conditions will be added as expected.', + wrongElementNode: "' method.", + invalidDataStructure: ': Used invalid data structure in updateCondition/updateGroup method.', + dateTabLabel: 'DATE', + timeTabLabel: 'TIME', + queryLabel: '', + }, +}; + +export const QueryBuilderOperations = { + EQUALS: { + label: 'Equals', + name: '=', + expressionTemplate: '{0} = "{1}"', + }, + DOES_NOT_EQUAL: { + label: 'Does not equal', + name: '<>', + expressionTemplate: '{0} != "{1}"', + }, + STARTS_WITH: { + label: 'Starts with', + name: 'startswith', + expressionTemplate: '{0}.StartsWith("{1}")', + }, + ENDS_WITH: { + label: 'Ends with', + name: 'endswith', + expressionTemplate: '{0}.EndsWith("{1}")', + }, + CONTAINS: { + label: 'Contains', + name: 'contains', + expressionTemplate: '{0}.Contains("{1}")', + }, + DOES_NOT_CONTAIN: { + label: 'Does not contain', + name: 'notcontains', + expressionTemplate: '!({0}.Contains("{1}"))', + }, + IS_BLANK: { + label: 'Is blank', + name: 'isblank', + expressionTemplate: 'string.IsNullOrEmpty({0})', + hideValue: true, + }, + IS_NOT_BLANK: { + label: 'Is not blank', + name: 'isnotblank', + expressionTemplate: '!string.IsNullOrEmpty({0})', + hideValue: true, + }, + GREATER_THAN: { + label: 'Greater than', + name: '>', + expressionTemplate: '{0} > "{1}"', + }, + GREATER_THAN_OR_EQUAL_TO: { + label: 'Greater than or equal to', + name: '>=', + expressionTemplate: '{0} >= "{1}"', + }, + LESS_THAN: { + label: 'Less than', + name: '<', + expressionTemplate: '{0} < "{1}"', + }, + LESS_THAN_OR_EQUAL_TO: { + label: 'Less than or equal to', + name: '<=', + expressionTemplate: '{0} <= "{1}"', + }, + // List expressions + LIST_EQUALS: { + label: 'Equals', + name: 'listequals', + expressionTemplate: '{0}.Contains("{1}")', + }, + LIST_DOES_NOT_EQUAL: { + label: 'Does not equal', + name: 'listnotequals', + expressionTemplate: '!({0}.Contains("{1}"))', + }, + LIST_CONTAINS: { + label: 'Contains', + name: 'listcontains', + expressionTemplate: '{0}.Any(it.Contains("{1}"))', + }, + LIST_DOES_NOT_CONTAIN: { + label: 'Does not contain', + name: 'listnotcontains', + expressionTemplate: '{0}.Any(!it.Contains("{1}"))', + }, + // Properties expressions + PROPERTY_EQUALS: { + label: 'Equals', + name: 'propertyequals', + expressionTemplate: 'properties["{0}"] = "{1}"', + }, + PROPERTY_DOES_NOT_EQUAL: { + label: 'Does not equal', + name: 'propertynotequals', + expressionTemplate: 'properties["{0}"] != "{1}"', + }, + PROPERTY_STARTS_WITH: { + label: 'Starts with', + name: 'propertystartswith', + expressionTemplate: 'properties["{0}"].StartsWith("{1}")', + }, + PROPERTY_ENDS_WITH: { + label: 'Ends with', + name: 'propertyendswith', + expressionTemplate: 'properties["{0}"].EndsWith("{1}")', + }, + PROPERTY_CONTAINS: { + label: 'Contains', + name: 'propertycontains', + expressionTemplate: 'properties["{0}"].Contains("{1}")', + }, + PROPERTY_DOES_NOT_CONTAIN: { + label: 'Does not contains', + name: 'propertynotcontains', + expressionTemplate: '!(properties["{0}"].Contains("{1}"))', + }, + PROPERTY_IS_BLANK: { + label: 'Is blank', + name: 'propertyisblank', + expressionTemplate: 'string.IsNullOrEmpty(properties["{0}"])', + hideValue: true, + }, + PROPERTY_IS_NOT_BLANK: { + label: 'Is not blank', + name: 'propertyisnotblank', + expressionTemplate: '!string.IsNullOrEmpty(properties["{0}"])', + hideValue: true, + }, +} + +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, +]; diff --git a/src/core/query-builder.utils.test.ts b/src/core/query-builder.utils.test.ts new file mode 100644 index 0000000..2b774e9 --- /dev/null +++ b/src/core/query-builder.utils.test.ts @@ -0,0 +1,100 @@ +import { expressionBuilderCallback, expressionReaderCallback, transformComputedFieldsQuery } from "./query-builder.utils" + +describe('QueryBuilderUtils', () => { + describe('transformComputedFieldsQuery', () => { + const computedDataFields = { + Object1: '(object1.prop1 = {value} || object1.prop2 = {value})', + Object2: '(object2.prop1 = {value} || object2.extra.prop2 = {value} || object2.prop3 = {value} || object2.prop4 = {value})', + Object3: '(object3.prop1 = {value} || object3.prop2 = {value} || object3.prop3 = {value})' + }; + + it('should transform a query with computed fields', () => { + const query = 'Object1 = "value1" AND Object2 = "value2"'; + const result = transformComputedFieldsQuery(query, computedDataFields); + expect(result).toBe('(object1.prop1 = value1 || object1.prop2 = value1) AND (object2.prop1 = value2 || object2.extra.prop2 = value2 || object2.prop3 = value2 || object2.prop4 = value2)'); + }); + + it('should return the original query if no computed fields are present', () => { + const query = 'field1 = "value1" AND field2 = "value2"'; + const result = transformComputedFieldsQuery(query, computedDataFields); + expect(result).toBe(query); + }); + + it('should handle multiple computed fields correctly', () => { + const query = 'Object1 = "value1" AND Object3 = "value3"'; + const result = transformComputedFieldsQuery(query, computedDataFields); + expect(result).toBe('(object1.prop1 = value1 || object1.prop2 = value1) AND (object3.prop1 = value3 || object3.prop2 = value3 || object3.prop3 = value3)'); + }); + + it('should handle an empty query', () => { + const query = ''; + const result = transformComputedFieldsQuery(query, computedDataFields); + expect(result).toBe(query); + }); + }); + + describe('expressionBuilderCallback', () => { + const mockQueryBuilderCustomOperation = { + expressionTemplate: '{0} = {1}' + }; + + it('should build a valid expression for a single field', () => { + const options = { + 'field1': [{ label: 'Option A', value: 'ValueA' }], + }; + + const result = expressionBuilderCallback(options).call(mockQueryBuilderCustomOperation, 'field1', 'someOperation', 'Option A'); + + expect(result).toBe('field1 = ValueA'); + }); + + it('should return original value if no matching label found', () => { + const options = { + 'field1': [{ label: 'Option A', value: 'ValueA' }], + }; + + const callback = expressionBuilderCallback(options).bind(mockQueryBuilderCustomOperation); + const result = callback('field1', 'someOperation', 'Option B'); + + expect(result).toBe('field1 = Option B'); + }); + + it('should return original expression if no options are provided', () => { + const options = {}; + + const callback = expressionBuilderCallback(options).bind(mockQueryBuilderCustomOperation); + const result = callback('field1', 'someOperation', 'Any Value'); + + expect(result).toBe('field1 = Any Value'); + }); + }) + + describe('expressionReaderCallback', () => { + const options = { + 'optionsObject1': [{ label: 'Label A', value: 'ValueA' }], + 'optionsObject2': [{ label: 'Label B', value: 'ValueB' }], + }; + + it('should map value to label for a given field', () => { + const callback = expressionReaderCallback(options); + const result = callback('someExpression', ['optionsObject1', 'ValueA']); + + expect(result).toEqual({ fieldName: 'optionsObject1', value: 'Label A' }); + }); + + it('should return original field name and value if no matching label is found', () => { + const callback = expressionReaderCallback(options); + const result = callback('someExpression', ['field1', 'NonExistentValue']); + + expect(result).toEqual({ fieldName: 'field1', value: 'NonExistentValue' }); + }); + + it('should return original field name and value if no options are provided for the field', () => { + const emptyOptions = {}; + const callback = expressionReaderCallback(emptyOptions); + const result = callback('someExpression', ['field1', 'ValueA']); + + expect(result).toEqual({ fieldName: 'field1', value: 'ValueA' }); + }); + }) +}) diff --git a/src/core/query-builder.utils.ts b/src/core/query-builder.utils.ts new file mode 100644 index 0000000..18fcc85 --- /dev/null +++ b/src/core/query-builder.utils.ts @@ -0,0 +1,64 @@ +import { QueryBuilderCustomOperation } from "smart-webcomponents-react"; +import { QueryBuilderOption } from "./types"; + +/** + * The function will replace the computed fields with their transformation + * Example: object = "value" => object1.prop1 = "value" || object1.prop2 = "value" + * @param query Query builder provided string + * @param computedDataFields Object with computed fields and their transformations + * @returns Updated query with computed fields transformed + */ +export function transformComputedFieldsQuery(query: string, computedDataFields: Record) { + for (const [field, transformation] of Object.entries(computedDataFields)) { + const regex = new RegExp(`\\b${field}\\s*=\\s*"([^"]*)"`, 'g'); + query = query.replace(regex, (_match, value) => transformation.replace(/{value}/g, value)); + } + + return query; +} + +/** + * The callback will replace the option's label with it's value + * @param options Object with value, label object properties, that hold the dropdown values + * @returns callback to be used by query builder when building the query + */ +export function expressionBuilderCallback(options: Record) { + return function (this: QueryBuilderCustomOperation, fieldName: string, _operation: string, value: string) { + const buildExpression = (field: string, value: string) => { + const fieldOptions = options[fieldName]; + if (fieldOptions?.length) { + const labelValue = fieldOptions.find(option => option.label === value); + + if (labelValue) { + value = labelValue.value; + } + } + + return this.expressionTemplate?.replace('{0}', field).replace('{1}', value); + }; + + return buildExpression(fieldName, value); + }; +} + +/** + * The callback will replace the option's value with it's label + * @param options Object with value, label object properties, that hold the dropdown values + * @returns callback to be used by query builder when reading the query + */ +export function expressionReaderCallback(options: Record) { + return function (_expression: string, [fieldName, value]: string[]) { + const fieldOptions = options[fieldName]; + + if (fieldOptions?.length) { + const valueLabel = fieldOptions.find(option => option.value === value); + + if (valueLabel) { + value = valueLabel.label; + } + } + + return { fieldName, value }; + } +}; + diff --git a/src/core/types.ts b/src/core/types.ts index d61a045..ae157e3 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -32,3 +32,8 @@ export interface SystemLinkError { name: string; } } + +export interface QueryBuilderOption { + label: string; + value: string; +} diff --git a/src/datasources/asset-calibration/AssetCalibrationDataSource.ts b/src/datasources/asset-calibration/AssetCalibrationDataSource.ts index 249b95b..f5ca0e7 100644 --- a/src/datasources/asset-calibration/AssetCalibrationDataSource.ts +++ b/src/datasources/asset-calibration/AssetCalibrationDataSource.ts @@ -11,37 +11,42 @@ import { AssetCalibrationForecastKey, AssetCalibrationQuery, AssetCalibrationTimeBasedGroupByType, - AssetModel, - AssetsResponse, CalibrationForecastResponse, } from './types'; -import { SystemMetadata } from "../system/types"; -import { defaultOrderBy, defaultProjection } from "../system/constants"; +import { transformComputedFieldsQuery } from 'core/query-builder.utils'; +import { AssetComputedDataFields } from './constants'; +import { AssetModel, AssetsResponse } from 'datasources/asset-common/types'; export class AssetCalibrationDataSource extends DataSourceBase { + public defaultQuery = { + groupBy: [], + filter: '' + }; + constructor( readonly instanceSettings: DataSourceInstanceSettings, readonly backendSrv: BackendSrv = getBackendSrv(), - readonly templateSrv: TemplateSrv = getTemplateSrv() + readonly templateSrv: TemplateSrv = getTemplateSrv(), ) { super(instanceSettings, backendSrv, templateSrv); } - defaultQuery = { - groupBy: [], - }; - baseUrl = this.instanceSettings.url + '/niapm/v1'; async runQuery(query: AssetCalibrationQuery, options: DataQueryRequest): Promise { + if (query.filter) { + query.filter = this.templateSrv.replace(transformComputedFieldsQuery(query.filter, AssetComputedDataFields), options.scopedVars); + } + return await this.processCalibrationForecastQuery(query as AssetCalibrationQuery, options); } + async processCalibrationForecastQuery(query: AssetCalibrationQuery, options: DataQueryRequest) { const result: DataFrameDTO = { refId: query.refId, fields: [] }; const from = options.range!.from.toISOString(); const to = options.range!.to.toISOString(); - const calibrationForecastResponse: CalibrationForecastResponse = await this.queryCalibrationForecast(query.groupBy, from, to); + const calibrationForecastResponse: CalibrationForecastResponse = await this.queryCalibrationForecast(query.groupBy, from, to, query.filter); result.fields = calibrationForecastResponse.calibrationForecast.columns || []; if (this.isGroupByTime(query)) { @@ -129,20 +134,6 @@ export class AssetCalibrationDataSource extends DataSourceBase { - try { - let response = await this.getSystems({ - filter: filter, - projection: `new(${projection.join()})`, - orderBy: defaultOrderBy, - }) - - return response.data; - } catch (error) { - throw new Error(`An error occurred while querying systems: ${error}`); - } - } - async testDatasource(): Promise { await this.get(this.baseUrl + '/assets?take=1'); return { status: 'success', message: 'Data source connected and authentication successful!' }; diff --git a/src/datasources/asset-calibration/components/AssetCalibrationQueryBuilder.test.tsx b/src/datasources/asset-calibration/components/AssetCalibrationQueryBuilder.test.tsx new file mode 100644 index 0000000..e69681d --- /dev/null +++ b/src/datasources/asset-calibration/components/AssetCalibrationQueryBuilder.test.tsx @@ -0,0 +1,36 @@ +import React, { ReactNode } from "react"; +import { AssetCalibrationQueryBuilder } from "./AssetCalibrationQueryBuilder"; +import { render } from "@testing-library/react"; +import { Workspace } from "core/types"; + +describe('AssetCalibrationQueryBuilder', () => { + describe('useEffects', () => { + let reactNode: ReactNode; + + const containerClass = 'smart-filter-group-condition-container' + + function renderElement(workspaces: Workspace[], filter?: string) { + reactNode = React.createElement(AssetCalibrationQueryBuilder, { workspaces, filter, onChange: jest.fn() }); + const renderResult = render(reactNode); + return { + renderResult, + conditionsContainer: renderResult.container.getElementsByClassName(`${containerClass}`) + }; + } + + it('should render empty query builder', () => { + const { renderResult, conditionsContainer } = renderElement([], ''); + expect(conditionsContainer.length).toBe(1); + expect(renderResult.findByLabelText('Empty condition row')).toBeTruthy(); + }) + + it('should populate query builder', () => { + const workspace = { id: '1', name: 'Selected workspace' } as Workspace + const { conditionsContainer } = renderElement([workspace], 'Workspace = "1" && ModelName = "SomeRandomModelName"'); + + expect(conditionsContainer?.length).toBe(2); + expect(conditionsContainer.item(0)?.textContent).toContain(workspace.name); + expect(conditionsContainer.item(1)?.textContent).toContain("SomeRandomModelName"); + }) + }) +}) diff --git a/src/datasources/asset-calibration/components/AssetCalibrationQueryBuilder.tsx b/src/datasources/asset-calibration/components/AssetCalibrationQueryBuilder.tsx new file mode 100644 index 0000000..49fcb66 --- /dev/null +++ b/src/datasources/asset-calibration/components/AssetCalibrationQueryBuilder.tsx @@ -0,0 +1,96 @@ +import React, { useEffect, useState } from 'react'; +import { QueryBuilder, QueryBuilderCustomOperation, QueryBuilderProps } from 'smart-webcomponents-react/querybuilder'; +import { useTheme2 } from '@grafana/ui'; + +import 'smart-webcomponents-react/source/styles/smart.dark-orange.css'; +import 'smart-webcomponents-react/source/styles/smart.orange.css'; +import 'smart-webcomponents-react/source/styles/components/smart.base.css'; +import 'smart-webcomponents-react/source/styles/components/smart.common.css'; +import 'smart-webcomponents-react/source/styles/components/smart.querybuilder.css'; + +import { AssetCalibrationFields, AssetCalibrationStaticFields } from '../constants'; +import { Workspace, QueryBuilderOption } from 'core/types'; +import { QBField } from '../types'; +import { queryBuilderMessages, QueryBuilderOperations } from 'core/query-builder.constants'; +import { expressionBuilderCallback, expressionReaderCallback } from 'core/query-builder.utils'; + +type AssetCalibrationQueryBuilderProps = QueryBuilderProps & + React.HTMLAttributes & { + filter?: string; + workspaces: Workspace[] + }; + +export const AssetCalibrationQueryBuilder: React.FC = ({ filter, onChange, workspaces }) => { + const theme = useTheme2(); + document.body.setAttribute('theme', theme.isDark ? 'dark-orange' : 'orange'); + + const [fields, setFields] = useState([]); + const [operations, setOperations] = useState([]); + + useEffect(() => { + if (workspaces.length) { + const workspaceField = getWorkspaceField(workspaces); + + const fields = [ + ...AssetCalibrationStaticFields, + workspaceField, + ]; + + setFields(fields); + + const options = Object.values(fields) + .reduce((accumulator, fieldConfig) => { + if (fieldConfig.lookup) { + accumulator[fieldConfig.dataField!] = fieldConfig.lookup.dataSource; + } + + return accumulator; + }, {} as Record); + + const callbacks = { + expressionBuilderCallback: expressionBuilderCallback(options), + expressionReaderCallback: expressionReaderCallback(options) + }; + + setOperations([ + { + ...QueryBuilderOperations.EQUALS, + ...callbacks, + }, + { + ...QueryBuilderOperations.DOES_NOT_EQUAL, + ...callbacks, + }, + QueryBuilderOperations.CONTAINS, + QueryBuilderOperations.DOES_NOT_CONTAIN + ]); + } + }, [workspaces]); + + return ( + + ); +}; + +function getWorkspaceField(workspaces: Workspace[]) { + const workspaceField = AssetCalibrationFields.WORKSPACE; + + return { + ...workspaceField, + lookup: { + ...workspaceField.lookup, + dataSource: [ + ...workspaceField.lookup?.dataSource || [], + ...workspaces.map(({ id, name }) => ({ label: name, value: id })) + ] + } + }; +} + + diff --git a/src/datasources/asset-calibration/components/AssetCalibrationQueryEditor.tsx b/src/datasources/asset-calibration/components/AssetCalibrationQueryEditor.tsx index a67dadf..171ced8 100644 --- a/src/datasources/asset-calibration/components/AssetCalibrationQueryEditor.tsx +++ b/src/datasources/asset-calibration/components/AssetCalibrationQueryEditor.tsx @@ -2,41 +2,58 @@ import { QueryEditorProps, SelectableValue, toOption } from '@grafana/data'; import { AssetCalibrationDataSource } from '../AssetCalibrationDataSource'; import { AssetCalibrationPropertyGroupByType, AssetCalibrationQuery, AssetCalibrationTimeBasedGroupByType } from '../types'; import { InlineField, MultiSelect } from '@grafana/ui'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { enumToOptions } from '../../../core/utils'; import _ from 'lodash'; +import { AssetCalibrationQueryBuilder } from './AssetCalibrationQueryBuilder'; +import { Workspace } from 'core/types'; +import { FloatingError, parseErrorMessage } from 'core/errors'; type Props = QueryEditorProps; export function AssetCalibrationQueryEditor({ query, onChange, onRunQuery, datasource }: Props) { query = datasource.prepareQuery(query) as AssetCalibrationQuery; + + const [workspaces, setWorkspaces] = useState([]); + const [error, setError] = useState(''); + + useEffect(() => { + const getWorkspaces = async () => { + const workspaces = await datasource.getWorkspaces(); + setWorkspaces(workspaces); + } + + getWorkspaces().catch(error => setError(parseErrorMessage(error) || 'Failed to fetch workspaces')); + }, [datasource]); + + const handleQueryChange = (value: AssetCalibrationQuery, runQuery: boolean): void => { + onChange(value); + if (runQuery) { + onRunQuery(); + } + }; + const handleGroupByChange = (items?: Array>): void => { if (!items || _.isEqual(query.groupBy, items)) { return; } - const handleQueryChange = (value: AssetCalibrationQuery, runQuery: boolean): void => { - onChange(value); - if (runQuery) { - onRunQuery(); - } - }; - let groupBy: string[] = []; let timeGrouping: string = null!; for (let item of items) { - if (item.value === AssetCalibrationTimeBasedGroupByType.Day || item.value === AssetCalibrationTimeBasedGroupByType.Week || item.value === AssetCalibrationTimeBasedGroupByType.Month) { - timeGrouping = item.value; - continue; + if (item.value === AssetCalibrationTimeBasedGroupByType.Day || item.value === AssetCalibrationTimeBasedGroupByType.Week || item.value === AssetCalibrationTimeBasedGroupByType.Month) { + timeGrouping = item.value; + continue; } - + groupBy.push(item.value!); } if (timeGrouping) { groupBy.push(timeGrouping); } + groupBy = groupBy.slice(-2); if (!_.isEqual(query.groupBy, groupBy)) { @@ -44,6 +61,13 @@ export function AssetCalibrationQueryEditor({ query, onChange, onRunQuery, datas } }; + function onParameterChange(ev: CustomEvent) { + if (query.filter !== ev.detail.linq) { + query.filter = ev.detail.linq; + handleQueryChange(query, true); + } + } + return (
@@ -54,6 +78,14 @@ export function AssetCalibrationQueryEditor({ query, onChange, onRunQuery, datas value={query.groupBy.map(toOption) || []} /> + + onParameterChange(event)}> + + +
); } diff --git a/src/datasources/asset-calibration/components/AssetQueryCalibrationEditor.test.tsx b/src/datasources/asset-calibration/components/AssetQueryCalibrationEditor.test.tsx index 2bb19a4..d64faa0 100644 --- a/src/datasources/asset-calibration/components/AssetQueryCalibrationEditor.test.tsx +++ b/src/datasources/asset-calibration/components/AssetQueryCalibrationEditor.test.tsx @@ -1,4 +1,4 @@ -import { screen, waitFor } from '@testing-library/react'; +import { act, screen, waitFor } from '@testing-library/react'; import { setupRenderer } from '../../../test/fixtures'; import { AssetCalibrationQuery, AssetCalibrationTimeBasedGroupByType, AssetCalibrationPropertyGroupByType } from '../types'; import { select } from 'react-select-event'; @@ -8,85 +8,89 @@ import { AssetCalibrationQueryEditor } from './AssetCalibrationQueryEditor'; class FakeAssetCalibrationDataSource extends AssetCalibrationDataSource { } -const render = setupRenderer(AssetCalibrationQueryEditor ,FakeAssetCalibrationDataSource); +describe('AssetCalibrationQueryEditor', () => { + const render = async (query: AssetCalibrationQuery) => { + return await act(async () => setupRenderer(AssetCalibrationQueryEditor, FakeAssetCalibrationDataSource)(query)); + } -it('renders with query type calibration forecast', async () => { - render({} as AssetCalibrationQuery); + it('renders with query type calibration forecast', async () => { + await render({} as AssetCalibrationQuery); - const groupBy = screen.getAllByRole('combobox')[0]; - expect(groupBy).not.toBeNull(); -}); + const groupBy = screen.getAllByRole('combobox')[0]; + expect(groupBy).not.toBeNull(); + }); -it('renders with query type calibration forecast and updates group by time', async () => { - const [onChange] = render({ - groupBy: [AssetCalibrationTimeBasedGroupByType.Month], - } as AssetCalibrationQuery); + it('renders with query type calibration forecast and updates group by time', async () => { + const [onChange] = await render({ + groupBy: [AssetCalibrationTimeBasedGroupByType.Month], + } as AssetCalibrationQuery); - // User selects group by day - const groupBy = screen.getAllByRole('combobox')[0]; - await select(groupBy, "Day", { container: document.body }); - await waitFor(() => { - expect(onChange).toHaveBeenCalledWith( - expect.objectContaining({ groupBy: [AssetCalibrationTimeBasedGroupByType.Day] }) - ); - }); + // User selects group by day + const groupBy = screen.getAllByRole('combobox')[0]; + await select(groupBy, "Day", { container: document.body }); + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ groupBy: [AssetCalibrationTimeBasedGroupByType.Day] }) + ); + }); - // User selects group by location and week, overrides time - await select(groupBy,"Week", { container: document.body }); - await waitFor(() => { - expect(onChange).toHaveBeenCalledWith( - expect.objectContaining({ - groupBy: [AssetCalibrationTimeBasedGroupByType.Week], - }) - ); - }); + // User selects group by location and week, overrides time + await select(groupBy, "Week", { container: document.body }); + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + groupBy: [AssetCalibrationTimeBasedGroupByType.Week], + }) + ); + }); - // User selects group by month, overrides time - await select(groupBy, "Month", { container: document.body }); - await waitFor(() => { - expect(onChange).toHaveBeenCalledWith( - expect.objectContaining({ - groupBy: [AssetCalibrationTimeBasedGroupByType.Month], - }) - ); + // User selects group by month, overrides time + await select(groupBy, "Month", { container: document.body }); + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + groupBy: [AssetCalibrationTimeBasedGroupByType.Month], + }) + ); + }); }); -}); -it('renders with query type calibration forecast and updates group by properties', async () => { - const [onChange] = render({ - refId: '', - groupBy: [], - } as AssetCalibrationQuery); + it('renders with query type calibration forecast and updates group by properties', async () => { + const [onChange] = await render({ + refId: '', + groupBy: [], + } as AssetCalibrationQuery); - // User selects group by location - const groupBy = screen.getAllByRole('combobox')[0]; - await select(groupBy, "Location", { container: document.body }); - await waitFor(() => { - expect(onChange).toHaveBeenCalledWith( - expect.objectContaining({ - groupBy: [AssetCalibrationPropertyGroupByType.Location], - }) - ); - }); + // User selects group by location + const groupBy = screen.getAllByRole('combobox')[0]; + await select(groupBy, "Location", { container: document.body }); + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + groupBy: [AssetCalibrationPropertyGroupByType.Location], + }) + ); + }); - // User select group by model - await select(groupBy, "Model", { container: document.body }); - await waitFor(() => { - expect(onChange).toHaveBeenCalledWith( - expect.objectContaining({ - groupBy: [AssetCalibrationPropertyGroupByType.Location, AssetCalibrationPropertyGroupByType.Model], - }) - ); - }); + // User select group by model + await select(groupBy, "Model", { container: document.body }); + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + groupBy: [AssetCalibrationPropertyGroupByType.Location, AssetCalibrationPropertyGroupByType.Model], + }) + ); + }); // User select group by day - await select(groupBy, "Day", { container: document.body }); - await waitFor(() => { - expect(onChange).toHaveBeenCalledWith( - expect.objectContaining({ - groupBy: [AssetCalibrationPropertyGroupByType.Model, AssetCalibrationTimeBasedGroupByType.Day], - }) - ); + await select(groupBy, "Day", { container: document.body }); + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + groupBy: [AssetCalibrationPropertyGroupByType.Model, AssetCalibrationTimeBasedGroupByType.Day], + }) + ); + }); }); -}); +}) diff --git a/src/datasources/asset-calibration/constants.ts b/src/datasources/asset-calibration/constants.ts new file mode 100644 index 0000000..7199c3b --- /dev/null +++ b/src/datasources/asset-calibration/constants.ts @@ -0,0 +1,81 @@ +import { QueryBuilderOperations } from "core/query-builder.constants"; +import { AssetTypeOptions, BusTypeOptions, QBField } from "./types"; + +export const AssetCalibrationFields: Record = { + LOCATION: { + label: 'Location', + dataField: 'Location', + filterOperations: [ + QueryBuilderOperations.EQUALS.name, + QueryBuilderOperations.DOES_NOT_EQUAL.name + ], + }, + WORKSPACE: { + label: 'Workspace', + dataField: 'Workspace', + dataType: 'string', + filterOperations: [ + QueryBuilderOperations.EQUALS.name, + QueryBuilderOperations.DOES_NOT_EQUAL.name + ], + lookup: { + dataSource: [] + } + }, + MODEL_NAME: { + label: 'Model Name', + dataField: 'ModelName', + filterOperations: [ + QueryBuilderOperations.EQUALS.name, + QueryBuilderOperations.DOES_NOT_EQUAL.name, + QueryBuilderOperations.CONTAINS.name, + QueryBuilderOperations.DOES_NOT_CONTAIN.name + ], + }, + VENDOR_NAME: { + label: 'Vendor Name', + dataField: 'VendorName', + filterOperations: [ + QueryBuilderOperations.EQUALS.name, + QueryBuilderOperations.DOES_NOT_EQUAL.name, + QueryBuilderOperations.CONTAINS.name, + QueryBuilderOperations.DOES_NOT_CONTAIN.name + ], + }, + BUS_TYPE: { + label: 'Bus Type', + dataField: 'BusType', + filterOperations: [ + QueryBuilderOperations.EQUALS.name, + QueryBuilderOperations.DOES_NOT_EQUAL.name + ], + lookup: { + dataSource: BusTypeOptions, + readonly: true, + } + }, + ASSET_TYPE: { + label: 'Asset Type', + dataField: 'AssetType', + filterOperations: [ + QueryBuilderOperations.EQUALS.name, + QueryBuilderOperations.DOES_NOT_EQUAL.name + ], + lookup: { + dataSource: AssetTypeOptions, + readonly: true, + }, + }, +}; + +export const AssetCalibrationStaticFields = [ + AssetCalibrationFields.LOCATION, + AssetCalibrationFields.MODEL_NAME, + AssetCalibrationFields.VENDOR_NAME, + AssetCalibrationFields.ASSET_TYPE, + AssetCalibrationFields.BUS_TYPE +]; + +export const AssetComputedDataFields = { + 'Location': '(Location.PhysicalLocation = "{value}" || Location.MinionId = "{value}")', +} diff --git a/src/datasources/asset-calibration/types.ts b/src/datasources/asset-calibration/types.ts index 4d3e0fc..3f31f21 100644 --- a/src/datasources/asset-calibration/types.ts +++ b/src/datasources/asset-calibration/types.ts @@ -1,8 +1,10 @@ import { FieldDTO } from '@grafana/data'; import { DataQuery } from '@grafana/schema' +import { QueryBuilderField } from 'smart-webcomponents-react'; export interface AssetCalibrationQuery extends DataQuery { groupBy: string[]; + filter?: string; } export enum AssetQueryLabel { @@ -30,64 +32,6 @@ export enum AssetCalibrationForecastKey { Count = "Assets" } -export enum EntityType { - Asset = "Asset", - System = "System" -} - -export interface AssetsResponse { - assets: AssetModel[], - totalCount: number -} - -export interface AssetModel { - modelName: string, - modelNumber: number, - serialNumber: string, - vendorName: string, - vendorNumber: number, - busType: 'BUILT_IN_SYSTEM' | 'PCI_PXI' | 'USB' | 'GPIB' | 'VXI' | 'SERIAL' | 'TCP_IP' | 'CRIO' | 'SCXI' | 'CDAQ' | 'SWITCH_BLOCK' | 'SCC' | 'FIRE_WIRE' | 'ACCESSORY' | 'CAN' | 'SWITCH_BLOCK_DEVICE' | 'SLSC' | string, - name: string, - assetType: 'GENERIC' | 'DEVICE_UNDER_TEST' | 'FIXTURE' | 'SYSTEM' | string, - firmwareVersion: string, - hardwareVersion: string, - visaResourceName: string, - temperatureSensors: any[], - supportsSelfCalibration: boolean, - supportsExternalCalibration: boolean, - customCalibrationInterval?: number, - selfCalibration?: any, - isNIAsset: boolean, - workspace: string, - fileIds: string[], - supportsSelfTest: boolean, - supportsReset: boolean, - id: string, - location: AssetLocationModel, - calibrationStatus: 'OK' | 'APPROACHING_RECOMMENDED_DUE_DATE' | 'PAST_RECOMMENDED_DUE_DATE' | string, - isSystemController: boolean, - externalCalibration?: ExternalCalibrationModel, - discoveryType: 'MANUAL' | 'AUTOMATIC' | string, - properties: Record, - keywords: string[], - lastUpdatedTimestamp: string, -} - -export interface AssetPresenceWithSystemConnectionModel { - assetPresence: "INITIALIZING" | "UNKNOWN" | "NOT_PRESENT" | "PRESENT" | string, - // to be compatible with both SLS and SLE - systemConnection?: "APPROVED" | "DISCONNECTED" | "CONNECTED_UPDATE_PENDING" | "CONNECTED_UPDATE_SUCCESSFUL" | "CONNECTED_UPDATE_FAILED" | "UNSUPPORTED" | "ACTIVATED" | "CONNECTED" | string -} - -export interface AssetLocationModel { - minionId: string, - parent: string, - resourceUri: string, - slotNumber: number, - state: AssetPresenceWithSystemConnectionModel, - physicalLocation?: string -} - export interface ExternalCalibrationModel { temperatureSensors: any[], isLimited?: boolean, @@ -109,37 +53,71 @@ export interface CalibrationForecastResponse { calibrationForecast: CalibrationForecastModel } -export enum AssetFilterProperties { - AssetIdentifier = 'AssetIdentifier', - SerialNumber = 'SerialNumber', - ModelName = 'ModelName', - VendorName = 'VendorName', - VendorNumber = 'VendorNumber', - AssetName = 'AssetName', - FirmwareVersion = 'FirmwareVersion', - HardwareVersion = 'HardwareVersion', - BusType = 'BusType', - IsNIAsset = 'IsNIAsset', - Keywords = 'Keywords', - Properties = 'Properties', - LocationMinionId = 'Location.MinionId', - LocationSlotNumber = 'Location.SlotNumber', - LocationAssetStateSystemConnection = 'Location.AssetState.SystemConnection', - LocationAssetStateAssetPresence = 'Location.AssetState.AssetPresence', - SupportsSelfCalibration = 'SupportsSelfCalibration', - SelfCalibrationCalibrationDate = 'SelfCalibration.CalibrationDate', - SupportsExternalCalibration = 'SupportsExternalCalibration', - CustomCalibrationInterval = 'CustomCalibrationInterval', - CalibrationStatus = 'CalibrationStatus', - ExternalCalibrationCalibrationDate = 'ExternalCalibration.CalibrationDate', - ExternalCalibrationNextRecommendedDate = 'ExternalCalibration.NextRecommendedDate', - ExternalCalibrationRecommendedInterval = 'ExternalCalibration.RecommendedInterval', - ExternalCalibrationComments = 'ExternalCalibration.Comments', - ExternalCalibrationIsLimited = 'ExternalCalibration.IsLimited', - ExternalCalibrationOperatorDisplayName = 'ExternalCalibration.Operator.DisplayName', - IsSystemController = 'IsSystemController' -} - export interface CalibrationForecastModel { columns: FieldDTO[], } + +export interface QBField extends QueryBuilderField { + lookup?: { + readonly?: boolean; + dataSource: Array<{ + label: string, + value: string + }>; + }, +} + +export enum BusType { + BUILT_IN_SYSTEM = 'BUILT_IN_SYSTEM', + PCI_PXI = 'PCI_PXI', + USB = 'USB', + GPIB = 'GPIB', + VXI = 'VXI', + SERIAL = 'SERIAL', + TCP_IP = 'TCP_IP', + CRIO = 'CRIO', + SCXI = 'SCXI', + CDAQ = 'CDAQ', + SWITCH_BLOCK = 'SWITCH_BLOCK', + SCC = 'SCC', + FIRE_WIRE = 'FIRE_WIRE', + ACCESSORY = 'ACCESSORY', + CAN = 'CAN', + SWITCH_BLOCK_DEVICE = 'SWITCH_BLOCK_DEVICE', + SLSC = 'SLSC' +} + +export const BusTypeOptions = [ + { label: 'Built-in-system', value: BusType.BUILT_IN_SYSTEM }, + { label: 'PCI/PXI', value: BusType.PCI_PXI }, + { label: 'USB', value: BusType.USB }, + { label: 'GPIB', value: BusType.GPIB }, + { label: 'VXI', value: BusType.VXI }, + { label: 'Serial', value: BusType.SERIAL }, + { label: 'TCP/IP', value: BusType.TCP_IP }, + { label: 'CompactRIO', value: BusType.CRIO }, + { label: 'SCXI', value: BusType.SCXI }, + { label: 'cDAQ', value: BusType.CDAQ }, + { label: 'SwitchBlock', value: BusType.SWITCH_BLOCK }, + { label: 'SCC', value: BusType.SCC }, + { label: 'FireWire', value: BusType.FIRE_WIRE }, + { label: 'ACCESSORY', value: BusType.ACCESSORY }, + { label: 'CAN', value: BusType.CAN }, + { label: 'SwitchBlock device', value: BusType.SWITCH_BLOCK_DEVICE }, + { label: 'SLSC', value: BusType.SLSC }, +]; + +export enum AssetType { + GENERIC = 'GENERIC', + DEVICE_UNDER_TEST = 'DEVICE_UNDER_TEST', + FIXTURE = 'FIXTURE', + SYSTEM = 'SYSTEM' +}; + +export const AssetTypeOptions = [ + { label: 'Generic', value: AssetType.GENERIC }, + { label: 'Device under test', value: AssetType.DEVICE_UNDER_TEST }, + { label: 'Fixture', value: AssetType.FIXTURE }, + { label: 'System', value: AssetType.SYSTEM }, +]; + diff --git a/src/datasources/asset-common/types.ts b/src/datasources/asset-common/types.ts new file mode 100644 index 0000000..b314bef --- /dev/null +++ b/src/datasources/asset-common/types.ts @@ -0,0 +1,70 @@ + +export interface AssetsResponse { + assets: AssetModel[], + totalCount: number + } + + export interface AssetLocationModel { + minionId: string, + parent: string, + resourceUri: string, + slotNumber: number, + state: AssetPresenceWithSystemConnectionModel, + physicalLocation?: string + } + + export interface AssetPresenceWithSystemConnectionModel { + assetPresence: "INITIALIZING" | "UNKNOWN" | "NOT_PRESENT" | "PRESENT" | string, + // to be compatible with both SLS and SLE + systemConnection?: "APPROVED" | "DISCONNECTED" | "CONNECTED_UPDATE_PENDING" | "CONNECTED_UPDATE_SUCCESSFUL" | "CONNECTED_UPDATE_FAILED" | "UNSUPPORTED" | "ACTIVATED" | "CONNECTED" | string + } + + export interface AssetModel { + modelName: string, + modelNumber: number, + serialNumber: string, + vendorName: string, + vendorNumber: number, + busType: 'BUILT_IN_SYSTEM' | 'PCI_PXI' | 'USB' | 'GPIB' | 'VXI' | 'SERIAL' | 'TCP_IP' | 'CRIO' | 'SCXI' | 'CDAQ' | 'SWITCH_BLOCK' | 'SCC' | 'FIRE_WIRE' | 'ACCESSORY' | 'CAN' | 'SWITCH_BLOCK_DEVICE' | 'SLSC' | string, + name: string, + assetType: 'GENERIC' | 'DEVICE_UNDER_TEST' | 'FIXTURE' | 'SYSTEM' | string, + firmwareVersion: string, + hardwareVersion: string, + visaResourceName: string, + temperatureSensors: any[], + supportsSelfCalibration: boolean, + supportsExternalCalibration: boolean, + customCalibrationInterval?: number, + selfCalibration?: any, + isNIAsset: boolean, + workspace: string, + fileIds: string[], + supportsSelfTest: boolean, + supportsReset: boolean, + id: string, + location: AssetLocationModel, + calibrationStatus: 'OK' | 'APPROACHING_RECOMMENDED_DUE_DATE' | 'PAST_RECOMMENDED_DUE_DATE' | string, + isSystemController: boolean, + externalCalibration?: ExternalCalibrationModel, + discoveryType: 'MANUAL' | 'AUTOMATIC' | string, + properties: Record, + keywords: string[], + lastUpdatedTimestamp: string, + } + + export interface ExternalCalibrationModel { + temperatureSensors: any[], + isLimited?: boolean, + date: string, + recommendedInterval: number, + nextRecommendedDate: string, + nextCustomDueDate?: string, + comments: string, + entryType: "AUTOMATIC" | "MANUAL" | string, + operator: ExternalCalibrationOperatorModel + } + + export interface ExternalCalibrationOperatorModel { + displayName: string; + userId: string + } diff --git a/src/datasources/asset/AssetDataSource.test.ts b/src/datasources/asset/AssetDataSource.test.ts index 84468ac..ef12392 100644 --- a/src/datasources/asset/AssetDataSource.test.ts +++ b/src/datasources/asset/AssetDataSource.test.ts @@ -8,11 +8,8 @@ import { setupDataSource, } from "test/fixtures"; import { AssetDataSource } from "./AssetDataSource"; -import { - AssetMetadataQuery, - AssetPresenceWithSystemConnectionModel, - AssetsResponse, -} from "./types"; +import { AssetMetadataQuery } from "./types"; +import { AssetPresenceWithSystemConnectionModel, AssetsResponse } from "datasources/asset-common/types"; let ds: AssetDataSource, backendSrv: MockProxy diff --git a/src/datasources/asset/AssetDataSource.ts b/src/datasources/asset/AssetDataSource.ts index fad97e2..a0f183e 100644 --- a/src/datasources/asset/AssetDataSource.ts +++ b/src/datasources/asset/AssetDataSource.ts @@ -9,12 +9,11 @@ import { DataSourceBase } from 'core/DataSourceBase'; import { AssetFilterProperties, AssetMetadataQuery, - AssetModel, - AssetsResponse, } from './types'; import { getWorkspaceName, replaceVariables } from "../../core/utils"; import { SystemMetadata } from "../system/types"; import { defaultOrderBy, defaultProjection } from "../system/constants"; +import { AssetModel, AssetsResponse } from 'datasources/asset-common/types'; export class AssetDataSource extends DataSourceBase { constructor( diff --git a/src/datasources/asset/types.ts b/src/datasources/asset/types.ts index b319b27..b298cc2 100644 --- a/src/datasources/asset/types.ts +++ b/src/datasources/asset/types.ts @@ -45,76 +45,6 @@ export interface CalibrationForecastModel { columns: FieldDTO[], } -export interface AssetsResponse { - assets: AssetModel[], - totalCount: number -} - -export interface AssetModel { - modelName: string, - modelNumber: number, - serialNumber: string, - vendorName: string, - vendorNumber: number, - busType: 'BUILT_IN_SYSTEM' | 'PCI_PXI' | 'USB' | 'GPIB' | 'VXI' | 'SERIAL' | 'TCP_IP' | 'CRIO' | 'SCXI' | 'CDAQ' | 'SWITCH_BLOCK' | 'SCC' | 'FIRE_WIRE' | 'ACCESSORY' | 'CAN' | 'SWITCH_BLOCK_DEVICE' | 'SLSC' | string, - name: string, - assetType: 'GENERIC' | 'DEVICE_UNDER_TEST' | 'FIXTURE' | 'SYSTEM' | string, - firmwareVersion: string, - hardwareVersion: string, - visaResourceName: string, - temperatureSensors: any[], - supportsSelfCalibration: boolean, - supportsExternalCalibration: boolean, - customCalibrationInterval?: number, - selfCalibration?: any, - isNIAsset: boolean, - workspace: string, - fileIds: string[], - supportsSelfTest: boolean, - supportsReset: boolean, - id: string, - location: AssetLocationModel, - calibrationStatus: 'OK' | 'APPROACHING_RECOMMENDED_DUE_DATE' | 'PAST_RECOMMENDED_DUE_DATE' | string, - isSystemController: boolean, - externalCalibration?: ExternalCalibrationModel, - discoveryType: 'MANUAL' | 'AUTOMATIC' | string, - properties: Record, - keywords: string[], - lastUpdatedTimestamp: string, -} - -export interface AssetPresenceWithSystemConnectionModel { - assetPresence: "INITIALIZING" | "UNKNOWN" | "NOT_PRESENT" | "PRESENT" | string, - // to be compatible with both SLS and SLE - systemConnection?: "APPROVED" | "DISCONNECTED" | "CONNECTED_UPDATE_PENDING" | "CONNECTED_UPDATE_SUCCESSFUL" | "CONNECTED_UPDATE_FAILED" | "UNSUPPORTED" | "ACTIVATED" | "CONNECTED" | string -} - -export interface AssetLocationModel { - minionId: string, - parent: string, - resourceUri: string, - slotNumber: number, - state: AssetPresenceWithSystemConnectionModel, - physicalLocation?: string -} - -export interface ExternalCalibrationModel { - temperatureSensors: any[], - isLimited?: boolean, - date: string, - recommendedInterval: number, - nextRecommendedDate: string, - nextCustomDueDate?: string, - comments: string, - entryType: "AUTOMATIC" | "MANUAL" | string, - operator: ExternalCalibrationOperatorModel -} - -export interface ExternalCalibrationOperatorModel { - displayName: string; - userId: string -} - export enum EntityType { Asset = "Asset", System = "System" diff --git a/src/datasources/notebook/QueryBuilder.tsx b/src/datasources/notebook/QueryBuilder.tsx index 447f767..d459278 100644 --- a/src/datasources/notebook/QueryBuilder.tsx +++ b/src/datasources/notebook/QueryBuilder.tsx @@ -1,6 +1,7 @@ import React, { useRef, useEffect } from 'react'; import { QueryBuilder, QueryBuilderProps } from 'smart-webcomponents-react/querybuilder'; import { useTheme2 } from '@grafana/ui'; +import { queryBuilderMessages, customOperations } from 'core/query-builder.constants'; import 'smart-webcomponents-react/source/styles/smart.dark-orange.css'; import 'smart-webcomponents-react/source/styles/smart.orange.css'; @@ -158,7 +159,7 @@ export const TestResultsQueryBuilder: React.FC = ( customOperations={customOperations} getDynamicField={getDynamicField} fields={fields} - messages={messages} + messages={queryBuilderMessages} showIcons // Only set value on first render {...(initialize.current && { value: props.defaultValue })} @@ -167,136 +168,6 @@ export const TestResultsQueryBuilder: React.FC = ( ); }; -const customOperations = [ - // Regular field expressions - { - label: 'Equals', - name: '=', - expressionTemplate: '{0} = "{1}"', - }, - { - label: 'Does not equal', - name: '<>', - expressionTemplate: '{0} != "{1}"', - }, - { - label: 'Starts with', - name: 'startswith', - expressionTemplate: '{0}.StartsWith("{1}")', - }, - { - label: 'Ends with', - name: 'endswith', - expressionTemplate: '{0}.EndsWith("{1}")', - }, - { - label: 'Contains', - name: 'contains', - expressionTemplate: '{0}.Contains("{1}")', - }, - { - label: 'Does not contain', - name: 'notcontains', - expressionTemplate: '!({0}.Contains("{1}"))', - }, - { - label: 'Is blank', - name: 'isblank', - expressionTemplate: 'string.IsNullOrEmpty({0})', - hideValue: true, - }, - { - label: 'Is not blank', - name: 'isnotblank', - expressionTemplate: '!string.IsNullOrEmpty({0})', - hideValue: true, - }, - { - label: 'Greater than', - name: '>', - expressionTemplate: '{0} > "{1}"', - }, - { - label: 'Greater than or equal to', - name: '>=', - expressionTemplate: '{0} >= "{1}"', - }, - { - label: 'Less than', - name: '<', - expressionTemplate: '{0} < "{1}"', - }, - { - label: 'Less than or equal to', - name: '<=', - expressionTemplate: '{0} <= "{1}"', - }, - // List expressions - { - label: 'Equals', - name: 'listequals', - expressionTemplate: '{0}.Contains("{1}")', - }, - { - label: 'Does not equal', - name: 'listnotequals', - expressionTemplate: '!({0}.Contains("{1}"))', - }, - { - label: 'Contains', - name: 'listcontains', - expressionTemplate: '{0}.Any(it.Contains("{1}"))', - }, - { - label: 'Does not contain', - name: 'listnotcontains', - expressionTemplate: '{0}.Any(!it.Contains("{1}"))', - }, - // Properties expressions - { - label: 'Equals', - name: 'propertyequals', - expressionTemplate: 'properties["{0}"] = "{1}"', - }, - { - label: 'Does not equal', - name: 'propertynotequals', - expressionTemplate: 'properties["{0}"] != "{1}"', - }, - { - label: 'Starts with', - name: 'propertystartswith', - expressionTemplate: 'properties["{0}"].StartsWith("{1}")', - }, - { - label: 'Ends with', - name: 'propertyendswith', - expressionTemplate: 'properties["{0}"].EndsWith("{1}")', - }, - { - label: 'Contains', - name: 'propertycontains', - expressionTemplate: 'properties["{0}"].Contains("{1}")', - }, - { - label: 'Does not contains', - name: 'propertynotcontains', - expressionTemplate: '!(properties["{0}"].Contains("{1}"))', - }, - { - label: 'Is blank', - name: 'propertyisblank', - expressionTemplate: 'string.IsNullOrEmpty(properties["{0}"])', - hideValue: true, - }, - { - label: 'Is not blank', - name: 'propertyisnotblank', - expressionTemplate: '!string.IsNullOrEmpty(properties["{0}"])', - hideValue: true, - }, -]; - const getDynamicField = () => ({ filterOperations: [ 'propertyequals', @@ -309,43 +180,3 @@ const getDynamicField = () => ({ 'propertyisnotblank', ], }); - -const messages = { - en: { - propertyUnknownType: "'' property is with undefined 'type' member!", - propertyInvalidValue: "Invalid '!", - propertyInvalidValueType: "Invalid '!", - elementNotInDOM: 'Element does not exist in DOM! Please, add the element to the DOM, before invoking a method.', - moduleUndefined: 'Module is undefined.', - missingReference: '.', - htmlTemplateNotSuported: ": Browser doesn't support HTMLTemplate elements.", - invalidTemplate: "' property accepts a string that must match the id of an HTMLTemplate element from the DOM.", - add: 'Add', - addCondition: 'Add Condition', - addGroup: 'Add Group', - and: 'And', - notand: 'Not And', - or: 'Or', - notor: 'Not Or', - '=': 'Equals', - '<>': 'Does not equal', - '>': 'Greater than', - '>=': 'Greater than or equal to', - '<': 'Less than', - '<=': 'Less than or equal to', - startswith: 'Starts with', - endswith: 'Ends with', - contains: 'Contains', - notcontains: 'Does not contain', - isblank: 'Is blank', - isnotblank: 'Is not blank', - wrongParentGroupIndex: "' method.", - missingFields: - ': Fields are required for proper condition\'s adding. Set "fields" source and then conditions will be added as expected.', - wrongElementNode: "' method.", - invalidDataStructure: ': Used invalid data structure in updateCondition/updateGroup method.', - dateTabLabel: 'DATE', - timeTabLabel: 'TIME', - queryLabel: '', - }, -};