From 4de79ff716ce20059502ae8dab946e756d0120f7 Mon Sep 17 00:00:00 2001 From: kkerezsi Date: Fri, 18 Oct 2024 00:38:49 +0300 Subject: [PATCH] feat(asset): add asset variable query with query builder (#90) Co-authored-by: NI\akerezsi --- src/datasources/asset/AssetDataSource.ts | 17 ++++++ .../asset/components/AssetQueryEditor.test.ts | 10 +-- .../list-assets/ListAssetsEditor.test.tsx | 14 ++--- .../AssetVariableQueryEditor.test.tsx | 61 +++++++++++++++++++ .../AssetVariableQueryEditor.tsx | 47 ++++++++++++++ src/datasources/asset/constants/constants.ts | 2 + .../asset/data-sources/AssetDataSourceBase.ts | 4 +- src/datasources/asset/module.ts | 4 +- .../asset/types/AssetVariableQuery.types.ts | 5 ++ src/datasources/asset/types/types.ts | 3 +- .../components/SystemQueryEditor.test.tsx | 18 +++--- 11 files changed, 160 insertions(+), 25 deletions(-) create mode 100644 src/datasources/asset/components/variable-editor/AssetVariableQueryEditor.test.tsx create mode 100644 src/datasources/asset/components/variable-editor/AssetVariableQueryEditor.tsx create mode 100644 src/datasources/asset/types/AssetVariableQuery.types.ts diff --git a/src/datasources/asset/AssetDataSource.ts b/src/datasources/asset/AssetDataSource.ts index 832ded1f..e1eb5856 100644 --- a/src/datasources/asset/AssetDataSource.ts +++ b/src/datasources/asset/AssetDataSource.ts @@ -2,6 +2,8 @@ import { DataFrameDTO, DataQueryRequest, DataSourceInstanceSettings, + LegacyMetricFindQueryOptions, + MetricFindValue, TestDataSourceResponse, } from '@grafana/data'; import { BackendSrv, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; @@ -17,6 +19,10 @@ import { CalibrationForecastQuery } from './types/CalibrationForecastQuery.types import { ListAssetsQuery } from './types/ListAssets.types'; import { ListAssetsDataSource } from './data-sources/list-assets/ListAssetsDataSource'; import { AssetSummaryDataSource } from './data-sources/asset-summary/AssetSummaryDataSource'; +import { AssetModel } from 'datasources/asset-common/types'; +import { QUERY_LIMIT } from './constants/constants'; +import { transformComputedFieldsQuery } from 'core/query-builder.utils'; +import { AssetVariableQuery } from './types/AssetVariableQuery.types'; export class AssetDataSource extends DataSourceBase { private assetSummaryDataSource: AssetSummaryDataSource; @@ -82,4 +88,15 @@ export class AssetDataSource extends DataSourceBase { + let assetFilter = query?.filter ?? ''; + assetFilter = transformComputedFieldsQuery( + this.templateSrv.replace(assetFilter, options.scopedVars), + this.listAssetsDataSource.assetComputedDataFields, + this.listAssetsDataSource.queryTransformationOptions + ); + const assetsResponse: AssetModel[] = await this.listAssetsDataSource.queryAssets(assetFilter, QUERY_LIMIT); + return assetsResponse.map((asset: AssetModel) => ({ text: asset.name, value: `Asset.${asset.vendorName}.${asset.modelName}.${asset.serialNumber}` })); + } } diff --git a/src/datasources/asset/components/AssetQueryEditor.test.ts b/src/datasources/asset/components/AssetQueryEditor.test.ts index d377bd6a..646988eb 100644 --- a/src/datasources/asset/components/AssetQueryEditor.test.ts +++ b/src/datasources/asset/components/AssetQueryEditor.test.ts @@ -1,4 +1,4 @@ -import { screen } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { SystemMetadata } from '../../system/types'; import { AssetDataSource } from '../AssetDataSource'; import { setupRenderer } from '../../../test/fixtures'; @@ -60,14 +60,14 @@ it('renders Asset list when feature is enabled', async () => { render({ queryType: AssetQueryType.ListAssets } as ListAssetsQuery); const queryType = screen.getAllByRole('combobox')[0]; await select(queryType, "List Assets", { container: document.body }); - expect(screen.getAllByText("List Assets").length).toBe(1) + await waitFor(() => expect(screen.getAllByText("List Assets").length).toBe(1)); }); it('does not render when Asset list feature is not enabled', async () => { assetDatasourceOptions.featureToggles.assetList = false; render({ queryType: AssetQueryType.ListAssets } as ListAssetsQuery); - expect(screen.getAllByRole('combobox').length).toBe(1); + await waitFor(() => expect(screen.getAllByRole('combobox').length).toBe(1)); }); it('renders Asset calibration forecast when feature is enabled', async () => { @@ -75,7 +75,7 @@ it('renders Asset calibration forecast when feature is enabled', async () => { render({ queryType: AssetQueryType.CalibrationForecast } as CalibrationForecastQuery); const queryType = screen.getAllByRole('combobox')[0]; await select(queryType, "Calibration Forecast", { container: document.body }); - expect(screen.getAllByText("Calibration Forecast").length).toBe(1) + await waitFor(() => expect(screen.getAllByText("Calibration Forecast").length).toBe(1)) }); it('renders Asset summary when feature is enabled', async () => { @@ -83,5 +83,5 @@ it('renders Asset summary when feature is enabled', async () => { render({ queryType: AssetQueryType.AssetSummary } as AssetSummaryQuery); const queryType = screen.getAllByRole('combobox')[0]; await select(queryType, "Asset Summary", { container: document.body }); - expect(screen.getAllByText("Asset Summary").length).toBe(1) + await waitFor(() => expect(screen.getAllByText("Asset Summary").length).toBe(1)); }); diff --git a/src/datasources/asset/components/editors/list-assets/ListAssetsEditor.test.tsx b/src/datasources/asset/components/editors/list-assets/ListAssetsEditor.test.tsx index 45cb06b4..eea1d489 100644 --- a/src/datasources/asset/components/editors/list-assets/ListAssetsEditor.test.tsx +++ b/src/datasources/asset/components/editors/list-assets/ListAssetsEditor.test.tsx @@ -1,4 +1,4 @@ -import { screen } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { SystemMetadata } from '../../../../system/types'; import { AssetDataSource } from '../../../AssetDataSource'; import { AssetQueryEditor } from '../../AssetQueryEditor'; @@ -21,7 +21,7 @@ const fakeSystems: SystemMetadata[] = [ ]; let assetDatasourceOptions = { - featureToggles: {...AssetFeatureTogglesDefaults} + featureToggles: { ...AssetFeatureTogglesDefaults } } class FakeAssetsSource extends ListAssetsDataSource { @@ -40,21 +40,21 @@ const render = setupRenderer(AssetQueryEditor, FakeAssetDataSource, () => assetD beforeEach(() => { assetDatasourceOptions = { - featureToggles: {...AssetFeatureTogglesDefaults} + featureToggles: { ...AssetFeatureTogglesDefaults } } }) it('does not render when feature is not enabled', async () => { assetDatasourceOptions.featureToggles.assetList = false; render({} as ListAssetsQuery); - expect(screen.getAllByRole('combobox').length).toBe(2); + await waitFor(() => expect(screen.getAllByRole('combobox').length).toBe(2)); }); it('renders the query builder', async () => { assetDatasourceOptions.featureToggles.assetList = true; render({} as ListAssetsQuery); - expect(screen.getAllByText('Property').length).toBe(1); - expect(screen.getAllByText('Operator').length).toBe(1); - expect(screen.getAllByText('Value').length).toBe(1); + await waitFor(() => expect(screen.getAllByText('Property').length).toBe(1)); + await waitFor(() => expect(screen.getAllByText('Operator').length).toBe(1)); + await waitFor(() => expect(screen.getAllByText('Value').length).toBe(1)); }); diff --git a/src/datasources/asset/components/variable-editor/AssetVariableQueryEditor.test.tsx b/src/datasources/asset/components/variable-editor/AssetVariableQueryEditor.test.tsx new file mode 100644 index 00000000..a09e96b9 --- /dev/null +++ b/src/datasources/asset/components/variable-editor/AssetVariableQueryEditor.test.tsx @@ -0,0 +1,61 @@ +import { screen, waitFor } from '@testing-library/react'; +import { AssetQuery } from '../../types/types'; +import { SystemMetadata } from '../../../system/types' +import { AssetVariableQueryEditor } from './AssetVariableQueryEditor'; +import { Workspace } from 'core/types'; +import { setupRenderer } from 'test/fixtures'; +import { ListAssetsDataSource } from '../../data-sources/list-assets/ListAssetsDataSource'; +import { AssetDataSource } from 'datasources/asset/AssetDataSource'; + +const fakeSystems: SystemMetadata[] = [ + { + id: '1', + state: 'CONNECTED', + workspace: '1', + }, + { + id: '2', + state: 'CONNECTED', + workspace: '2', + }, +]; + +const fakeWorkspaces: Workspace[] = [ + { + id: '1', + name: 'workspace1', + default: false, + enabled: true + }, + { + id: '2', + name: 'workspace2', + default: false, + enabled: true + }, +]; + +class FakeAssetsSource extends ListAssetsDataSource { + getWorkspaces(): Promise { + return Promise.resolve(fakeWorkspaces); + } + querySystems(filter?: string, projection?: string[]): Promise { + return Promise.resolve(fakeSystems); + } +} + +class FakeAssetDataSource extends AssetDataSource { + getListAssetsSource(): ListAssetsDataSource { + return new FakeAssetsSource(this.instanceSettings, this.backendSrv, this.templateSrv); + } +} + +const render = setupRenderer(AssetVariableQueryEditor, FakeAssetDataSource, () => {}); + +it('renders the variable query builder', async () => { + render({ filter: "" } as AssetQuery); + + await waitFor(() => expect(screen.getAllByText('Property').length).toBe(1)); + await waitFor(() => expect(screen.getAllByText('Operator').length).toBe(1)); + await waitFor(() => expect(screen.getAllByText('Value').length).toBe(1)); +}); diff --git a/src/datasources/asset/components/variable-editor/AssetVariableQueryEditor.tsx b/src/datasources/asset/components/variable-editor/AssetVariableQueryEditor.tsx new file mode 100644 index 00000000..34ec89c7 --- /dev/null +++ b/src/datasources/asset/components/variable-editor/AssetVariableQueryEditor.tsx @@ -0,0 +1,47 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { QueryEditorProps } from "@grafana/data"; +import { AssetDataSourceOptions, AssetQuery } from '../../../asset/types/types'; +import { AssetDataSource } from '../../AssetDataSource' +import { FloatingError } from '../../../../core/errors'; +import { AssetQueryBuilder } from '../editors/list-assets/query-builder/AssetQueryBuilder'; +import { Workspace } from '../../../../core/types'; +import { SystemMetadata } from '../../../system/types'; +import { AssetVariableQuery } from '../../../asset/types/AssetVariableQuery.types'; + +type Props = QueryEditorProps; + +export function AssetVariableQueryEditor({ datasource, query, onChange }: Props) { + const [workspaces, setWorkspaces] = useState([]); + const [systems, setSystems] = useState([]); + const [areDependenciesLoaded, setAreDependenciesLoaded] = useState(false); + const assetVariableQuery = query as AssetVariableQuery; + const assetListDatasource = useRef(datasource.getListAssetsSource()); + + useEffect(() => { + Promise.all([assetListDatasource.current.areSystemsLoaded$, assetListDatasource.current.areWorkspacesLoaded$]).then(() => { + setWorkspaces(Array.from(assetListDatasource.current.workspacesCache.values())); + setSystems(Array.from(assetListDatasource.current.systemAliasCache.values())); + setAreDependenciesLoaded(true); + }); + }, [datasource]); + + function onParameterChange(ev: CustomEvent) { + if (assetVariableQuery?.filter !== ev.detail.linq) { + onChange({ ...assetVariableQuery, filter: ev.detail.linq }); + } + } + + return ( +
+ onParameterChange(event)} + > + +
+ ); +} diff --git a/src/datasources/asset/constants/constants.ts b/src/datasources/asset/constants/constants.ts index 7b81b127..5254ce5d 100644 --- a/src/datasources/asset/constants/constants.ts +++ b/src/datasources/asset/constants/constants.ts @@ -6,3 +6,5 @@ export enum AllFieldNames { BUS_TYPE = 'BusType', ASSET_TYPE = 'AssetType', } + +export const QUERY_LIMIT = 1000; diff --git a/src/datasources/asset/data-sources/AssetDataSourceBase.ts b/src/datasources/asset/data-sources/AssetDataSourceBase.ts index 0d361f11..5f61ea84 100644 --- a/src/datasources/asset/data-sources/AssetDataSourceBase.ts +++ b/src/datasources/asset/data-sources/AssetDataSourceBase.ts @@ -90,11 +90,11 @@ export abstract class AssetDataSourceBase extends DataSourceBase>([ + public readonly queryTransformationOptions = new Map>([ [AllFieldNames.LOCATION, this.systemAliasCache] ]); - protected readonly assetComputedDataFields = new Map([ + public readonly assetComputedDataFields = new Map([ ...Object.values(AllFieldNames).map(field => [field, this.multipleValuesQuery(field)] as [string, ExpressionTransformFunction]), [ AllFieldNames.LOCATION, diff --git a/src/datasources/asset/module.ts b/src/datasources/asset/module.ts index c346c745..c9ef3a61 100644 --- a/src/datasources/asset/module.ts +++ b/src/datasources/asset/module.ts @@ -3,8 +3,10 @@ import { AssetDataSource } from './AssetDataSource'; import { AssetQueryEditor } from './components/AssetQueryEditor'; import { AssetDataSourceOptions, AssetQuery } from './types/types'; import { AssetConfigEditor } from './AssetConfigEditor'; +import { AssetVariableQueryEditor } from './components/variable-editor/AssetVariableQueryEditor' export const plugin = new DataSourcePlugin(AssetDataSource) .setConfigEditor(AssetConfigEditor) - .setQueryEditor(AssetQueryEditor); + .setQueryEditor(AssetQueryEditor) + .setVariableQueryEditor(AssetVariableQueryEditor); diff --git a/src/datasources/asset/types/AssetVariableQuery.types.ts b/src/datasources/asset/types/AssetVariableQuery.types.ts new file mode 100644 index 00000000..ac4518bf --- /dev/null +++ b/src/datasources/asset/types/AssetVariableQuery.types.ts @@ -0,0 +1,5 @@ +import { DataQuery } from '@grafana/schema' + +export interface AssetVariableQuery extends DataQuery { + filter: string; +} diff --git a/src/datasources/asset/types/types.ts b/src/datasources/asset/types/types.ts index 7f0098d6..b8eeac37 100644 --- a/src/datasources/asset/types/types.ts +++ b/src/datasources/asset/types/types.ts @@ -2,6 +2,7 @@ import { DataSourceJsonData } from "@grafana/data"; import { AssetSummaryQuery } from "./AssetSummaryQuery.types"; import { CalibrationForecastQuery } from "./CalibrationForecastQuery.types"; import { ListAssetsQuery } from "./ListAssets.types"; +import { AssetVariableQuery } from "./AssetVariableQuery.types"; export enum AssetQueryType { None = "", @@ -10,7 +11,7 @@ export enum AssetQueryType { AssetSummary = "Asset Summary" } -export type AssetQuery = ListAssetsQuery | CalibrationForecastQuery | AssetSummaryQuery; +export type AssetQuery = ListAssetsQuery | CalibrationForecastQuery | AssetSummaryQuery | AssetVariableQuery; export interface AssetFeatureToggles { calibrationForecast: boolean; diff --git a/src/datasources/system/components/SystemQueryEditor.test.tsx b/src/datasources/system/components/SystemQueryEditor.test.tsx index f9b19128..d3672573 100644 --- a/src/datasources/system/components/SystemQueryEditor.test.tsx +++ b/src/datasources/system/components/SystemQueryEditor.test.tsx @@ -1,4 +1,4 @@ -import { screen } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { setupRenderer } from "test/fixtures"; import { SystemDataSource } from "../SystemDataSource"; import { SystemQueryEditor } from "./SystemQueryEditor"; @@ -7,18 +7,18 @@ import userEvent from '@testing-library/user-event'; const render = setupRenderer(SystemQueryEditor, SystemDataSource); -it('renders with query defaults', () => { +it('renders with query defaults', async () => { render({} as SystemQuery); - expect(screen.getByRole('radio', { name: 'Summary' })).toBeChecked(); - expect(screen.queryByLabelText('System')).not.toBeInTheDocument(); + await waitFor(() => expect(screen.getByRole('radio', { name: 'Summary' })).toBeChecked()); + await waitFor(() => expect(screen.queryByLabelText('System')).not.toBeInTheDocument()); }); it('renders with saved metadata query', async () => { render({ queryKind: SystemQueryType.Metadata, systemName: 'my-system', workspace: '' }); - expect(screen.getByRole('radio', { name: 'Metadata' })).toBeChecked(); - expect(screen.queryByLabelText('System')).toHaveValue('my-system'); + await waitFor(() => expect(screen.getByRole('radio', { name: 'Metadata' })).toBeChecked()); + await waitFor(() => expect(screen.queryByLabelText('System')).toHaveValue('my-system')); }); it('updates when user interacts with fields', async () => { @@ -26,10 +26,10 @@ it('updates when user interacts with fields', async () => { // User changes query type await userEvent.click(screen.getByRole('radio', { name: 'Metadata' })); - expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ queryKind: SystemQueryType.Metadata })); - expect(screen.getByPlaceholderText('All systems')).toBeInTheDocument(); + await waitFor(() => expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ queryKind: SystemQueryType.Metadata }))); + await waitFor(() => expect(screen.getByPlaceholderText('All systems')).toBeInTheDocument()); // User types system name await userEvent.type(screen.getByLabelText('System'), 'my-system{enter}'); - expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ systemName: 'my-system' })); + await waitFor(() => expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ systemName: 'my-system' }))); });