Skip to content

Commit

Permalink
feat(asset): add asset variable query with query builder (#90)
Browse files Browse the repository at this point in the history
Co-authored-by: NI\akerezsi <[email protected]>
  • Loading branch information
kkerezsi and alexkerezsini authored Oct 17, 2024
1 parent a315a29 commit 4de79ff
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 25 deletions.
17 changes: 17 additions & 0 deletions src/datasources/asset/AssetDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
DataFrameDTO,
DataQueryRequest,
DataSourceInstanceSettings,
LegacyMetricFindQueryOptions,
MetricFindValue,
TestDataSourceResponse,
} from '@grafana/data';
import { BackendSrv, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
Expand All @@ -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<AssetQuery, AssetDataSourceOptions> {
private assetSummaryDataSource: AssetSummaryDataSource;
Expand Down Expand Up @@ -82,4 +88,15 @@ export class AssetDataSource extends DataSourceBase<AssetQuery, AssetDataSourceO
getListAssetsSource(): ListAssetsDataSource {
return this.listAssetsDataSource;
}

async metricFindQuery(query: AssetVariableQuery, options: LegacyMetricFindQueryOptions): Promise<MetricFindValue[]> {
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}` }));
}
}
10 changes: 5 additions & 5 deletions src/datasources/asset/components/AssetQueryEditor.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -60,28 +60,28 @@ 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 () => {
assetDatasourceOptions.featureToggles.calibrationForecast = true;
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 () => {
assetDatasourceOptions.featureToggles.assetSummary = true;
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));
});
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -21,7 +21,7 @@ const fakeSystems: SystemMetadata[] = [
];

let assetDatasourceOptions = {
featureToggles: {...AssetFeatureTogglesDefaults}
featureToggles: { ...AssetFeatureTogglesDefaults }
}

class FakeAssetsSource extends ListAssetsDataSource {
Expand All @@ -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));
});
Original file line number Diff line number Diff line change
@@ -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<Workspace[]> {
return Promise.resolve(fakeWorkspaces);
}
querySystems(filter?: string, projection?: string[]): Promise<SystemMetadata[]> {
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));
});
Original file line number Diff line number Diff line change
@@ -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<AssetDataSource, AssetQuery, AssetDataSourceOptions>;

export function AssetVariableQueryEditor({ datasource, query, onChange }: Props) {
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
const [systems, setSystems] = useState<SystemMetadata[]>([]);
const [areDependenciesLoaded, setAreDependenciesLoaded] = useState<boolean>(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 (
<div style={{ width: "525px" }}>
<AssetQueryBuilder
filter={assetVariableQuery.filter}
workspaces={workspaces}
systems={systems}
globalVariableOptions={assetListDatasource.current.globalVariableOptions}
areDependenciesLoaded={areDependenciesLoaded}
onChange={(event: any) => onParameterChange(event)}
></AssetQueryBuilder>
<FloatingError message={assetListDatasource.current.error} />
</div>
);
}
2 changes: 2 additions & 0 deletions src/datasources/asset/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export enum AllFieldNames {
BUS_TYPE = 'BusType',
ASSET_TYPE = 'AssetType',
}

export const QUERY_LIMIT = 1000;
4 changes: 2 additions & 2 deletions src/datasources/asset/data-sources/AssetDataSourceBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,11 @@ export abstract class AssetDataSourceBase extends DataSourceBase<AssetQuery, Ass
this.workspacesLeaded();
}

protected readonly queryTransformationOptions = new Map<string, Map<string, unknown>>([
public readonly queryTransformationOptions = new Map<string, Map<string, unknown>>([
[AllFieldNames.LOCATION, this.systemAliasCache]
]);

protected readonly assetComputedDataFields = new Map<string, ExpressionTransformFunction>([
public readonly assetComputedDataFields = new Map<string, ExpressionTransformFunction>([
...Object.values(AllFieldNames).map(field => [field, this.multipleValuesQuery(field)] as [string, ExpressionTransformFunction]),
[
AllFieldNames.LOCATION,
Expand Down
4 changes: 3 additions & 1 deletion src/datasources/asset/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, AssetQuery, AssetDataSourceOptions>(AssetDataSource)
.setConfigEditor(AssetConfigEditor)
.setQueryEditor(AssetQueryEditor);
.setQueryEditor(AssetQueryEditor)
.setVariableQueryEditor(AssetVariableQueryEditor);
5 changes: 5 additions & 0 deletions src/datasources/asset/types/AssetVariableQuery.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { DataQuery } from '@grafana/schema'

export interface AssetVariableQuery extends DataQuery {
filter: string;
}
3 changes: 2 additions & 1 deletion src/datasources/asset/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "",
Expand All @@ -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;
Expand Down
18 changes: 9 additions & 9 deletions src/datasources/system/components/SystemQueryEditor.test.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -7,29 +7,29 @@ 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 () => {
const [onChange] = render({ queryKind: SystemQueryType.Summary, systemName: '', workspace: '' });

// 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' })));
});

0 comments on commit 4de79ff

Please sign in to comment.