Skip to content

Commit

Permalink
feat(asset-calibration): added systems as dropdown values for location
Browse files Browse the repository at this point in the history
  • Loading branch information
Robert Aradei committed Oct 4, 2024
1 parent ca6e609 commit 09950d0
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 56 deletions.
41 changes: 35 additions & 6 deletions src/core/query-builder.utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { ExpressionTransformFunction } from "datasources/asset-calibration/types";
import { expressionBuilderCallback, expressionReaderCallback, transformComputedFieldsQuery } from "./query-builder.utils"
import TTLCache from "@isaacs/ttlcache";

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})'
const mockTransformation: ExpressionTransformFunction = (value, operation, _options) => {
return `obj.prop1 ${operation} ${value}`;
};

const computedDataFields = new Map<string, ExpressionTransformFunction>([
['Object1', mockTransformation],
['Object2', mockTransformation],
['Object3', mockTransformation],
]);

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)');
expect(result).toBe('obj.prop1 = value1 AND obj.prop1 = value2');
});

it('should return the original query if no computed fields are present', () => {
Expand All @@ -23,14 +29,37 @@ describe('QueryBuilderUtils', () => {
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)');
expect(result).toBe('obj.prop1 = value1 AND obj.prop1 = value3');
});

it('should handle an empty query', () => {
const query = '';
const result = transformComputedFieldsQuery(query, computedDataFields);
expect(result).toBe(query);
});

it('should handle unsupported operations correctly', () => {
const query = 'Object1 > "value1" AND Object2 < "value2"';
const result = transformComputedFieldsQuery(query, computedDataFields);
expect(result).toBe(query);
});

it('should handle supported operations correctly', () => {
const query = 'Object1 != "value1" AND Object2 = "value2"';
const result = transformComputedFieldsQuery(query, computedDataFields);
expect(result).toBe('obj.prop1 != value1 AND obj.prop1 = value2');
});

it('should handle options correctly', () => {
const options = new Map<string, TTLCache<string, unknown>>();
const cache = new TTLCache<string, unknown>();
cache.set('key', 'value', { ttl: 1 });
options.set('Object1', cache);

const query = 'Object1 = "value1"';
const result = transformComputedFieldsQuery(query, computedDataFields, options);
expect(result).toBe('obj.prop1 = value1');
});
});

describe('expressionBuilderCallback', () => {
Expand Down
19 changes: 15 additions & 4 deletions src/core/query-builder.utils.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import { QueryBuilderCustomOperation } from "smart-webcomponents-react";
import { QueryBuilderOption } from "./types";
import TTLCache from "@isaacs/ttlcache";
import { ExpressionTransformFunction } from "datasources/asset-calibration/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
* @param options Object with options for the computed fields
* @returns Updated query with computed fields transformed
*/
export function transformComputedFieldsQuery(query: string, computedDataFields: Record<string, string>) {
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));
export function transformComputedFieldsQuery(
query: string,
computedDataFields: Map<string, ExpressionTransformFunction>,
options?: Map<string, TTLCache<string, unknown>>
) {
for (const [field, transformation] of computedDataFields.entries()) {
const supportedOperations = ['=', '!='];
const regex = new RegExp(`\\b${field}\\s*(${supportedOperations.join('|')})\\s*"([^"]*)"`, 'g');

query = query.replace(regex, (_match, operation, value) => {
return transformation(value, operation, options?.get(field));
});
}

return query;
Expand Down
25 changes: 23 additions & 2 deletions src/datasources/asset-calibration/AssetCalibrationDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ import {
AssetCalibrationTimeBasedGroupByType,
CalibrationForecastResponse,
ColumnDescriptorType,
ExpressionTransformFunction,
FieldDTOWithDescriptor,
} from './types';
import { transformComputedFieldsQuery } from 'core/query-builder.utils';
import { AssetComputedDataFields } from './constants';
import { AssetCalibrationFieldNames } from './constants';
import { AssetModel, AssetsResponse } from 'datasources/asset-common/types';
import TTLCache from '@isaacs/ttlcache';
import { metadataCacheTTL } from 'datasources/data-frame/constants';
import { SystemMetadata } from 'datasources/system/types';
import { defaultOrderBy, defaultProjection } from 'datasources/system/constants';
import { QueryBuilderOperations } from 'core/query-builder.constants';

export class AssetCalibrationDataSource extends DataSourceBase<AssetCalibrationQuery> {
public defaultQuery = {
Expand All @@ -41,11 +43,30 @@ export class AssetCalibrationDataSource extends DataSourceBase<AssetCalibrationQ

systemAliasCache: TTLCache<string, SystemMetadata> = new TTLCache<string, SystemMetadata>({ ttl: metadataCacheTTL });

private readonly assetComputedDataFields = new Map<AssetCalibrationFieldNames, ExpressionTransformFunction>([
[
AssetCalibrationFieldNames.LOCATION,
(value: string, operation: string, options?: TTLCache<string, unknown>) => {
if (options?.has(value)) {
return `Location.MinionId ${operation} "${value}"`
}

const logicalOperator = operation === QueryBuilderOperations.EQUALS.name ? '||' : '&&';
return `(Location.MinionId ${operation} "${value}" ${logicalOperator} Location.PhysicalLocation ${operation} "${value}")`;
}
],
]);

private readonly queryTransformationOptions = new Map<AssetCalibrationFieldNames, TTLCache<string, unknown>>([
[AssetCalibrationFieldNames.LOCATION, this.systemAliasCache]
]);

async runQuery(query: AssetCalibrationQuery, options: DataQueryRequest): Promise<DataFrameDTO> {
await this.loadSystems();

if (query.filter) {
query.filter = this.templateSrv.replace(transformComputedFieldsQuery(query.filter, AssetComputedDataFields), options.scopedVars);
const transformedQuery = transformComputedFieldsQuery(query.filter, this.assetComputedDataFields, this.queryTransformationOptions);
query.filter = this.templateSrv.replace(transformedQuery, options.scopedVars);
}

return await this.processCalibrationForecastQuery(query as AssetCalibrationQuery, options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import React, { ReactNode } from "react";
import { AssetCalibrationQueryBuilder } from "./AssetCalibrationQueryBuilder";
import { render } from "@testing-library/react";
import { Workspace } from "core/types";
import { SystemMetadata } from "datasources/system/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() });
function renderElement(workspaces: Workspace[], systems: SystemMetadata[], filter?: string) {
reactNode = React.createElement(AssetCalibrationQueryBuilder, { workspaces, systems, filter, onChange: jest.fn() });
const renderResult = render(reactNode);
return {
renderResult,
Expand All @@ -19,18 +20,29 @@ describe('AssetCalibrationQueryBuilder', () => {
}

it('should render empty query builder', () => {
const { renderResult, conditionsContainer } = renderElement([], '');
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"');
it('should select workspace in query builder', () => {
const workspace = { id: '1', name: 'Selected workspace' } as Workspace;
const system = { id: '1', alias: 'Selected system' } as SystemMetadata;
const { conditionsContainer } = renderElement([workspace], [system], 'Workspace = "1" && ModelName = "SomeRandomModelName"');

expect(conditionsContainer?.length).toBe(2);
expect(conditionsContainer.item(0)?.textContent).toContain(workspace.name);
expect(conditionsContainer.item(1)?.textContent).toContain("SomeRandomModelName");
})
})
})

it('should select system in query builder', () => {
const workspace = { id: '1', name: 'Selected workspace' } as Workspace;
const system = { id: '1', alias: 'Selected system' } as SystemMetadata;

const { conditionsContainer } = renderElement([workspace], [system], 'Location = "1"');

expect(conditionsContainer?.length).toBe(1);
expect(conditionsContainer.item(0)?.textContent).toContain(system.alias);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,31 @@ 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';
import { SystemMetadata } from 'datasources/system/types';

type AssetCalibrationQueryBuilderProps = QueryBuilderProps &
React.HTMLAttributes<Element> & {
filter?: string;
workspaces: Workspace[]
workspaces: Workspace[],
systems: SystemMetadata[],
};

export const AssetCalibrationQueryBuilder: React.FC<AssetCalibrationQueryBuilderProps> = ({ filter, onChange, workspaces }) => {
export const AssetCalibrationQueryBuilder: React.FC<AssetCalibrationQueryBuilderProps> = ({ filter, onChange, workspaces, systems }) => {
const theme = useTheme2();
document.body.setAttribute('theme', theme.isDark ? 'dark-orange' : 'orange');

const [fields, setFields] = useState<QBField[]>([]);
const [operations, setOperations] = useState<QueryBuilderCustomOperation[]>([]);

useEffect(() => {
if (workspaces.length) {
if (workspaces.length && systems.length) {
const workspaceField = getWorkspaceField(workspaces);
const locationField = getLocationField(systems);

const fields = [
...AssetCalibrationStaticFields,
workspaceField,
locationField,
...AssetCalibrationStaticFields,
];

setFields(fields);
Expand Down Expand Up @@ -65,7 +69,7 @@ export const AssetCalibrationQueryBuilder: React.FC<AssetCalibrationQueryBuilder
QueryBuilderOperations.DOES_NOT_CONTAIN
]);
}
}, [workspaces]);
}, [workspaces, systems]);

return (
<QueryBuilder
Expand Down Expand Up @@ -93,4 +97,19 @@ function getWorkspaceField(workspaces: Workspace[]) {
};
}

function getLocationField(systems: SystemMetadata[]) {
const locationField = AssetCalibrationFields.LOCATION;

return {
...locationField,
lookup: {
...locationField.lookup,
dataSource: [
...locationField.lookup?.dataSource || [],
...systems.map(({ id, alias }) => ({ label: alias || id, value: id }))
]
}
};
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.asset-calibration-forecast {
margin-top: 10px;
display: flex;
column-gap: 100px;
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { QueryEditorProps, SelectableValue, toOption } from '@grafana/data';
import { AssetCalibrationDataSource } from '../AssetCalibrationDataSource';
import { AssetCalibrationPropertyGroupByType, AssetCalibrationQuery, AssetCalibrationTimeBasedGroupByType } from '../types';
import { InlineField, MultiSelect } from '@grafana/ui';
import { InlineField, Label, MultiSelect } from '@grafana/ui';
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';
import { SystemMetadata } from 'datasources/system/types';
import './AssetCalibrationQueryEditor.scss';

type Props = QueryEditorProps<AssetCalibrationDataSource, AssetCalibrationQuery>;

export function AssetCalibrationQueryEditor({ query, onChange, onRunQuery, datasource }: Props) {
export const AssetCalibrationQueryEditor = ({ query, onChange, onRunQuery, datasource }: Props) => {
query = datasource.prepareQuery(query) as AssetCalibrationQuery;
const areSystemsLoaded = datasource.systemAliasCache.size > 0;

const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
const [systems, setSystems] = useState<SystemMetadata[]>([]);
const [error, setError] = useState<string>('');

useEffect(() => {
Expand All @@ -23,8 +27,9 @@ export function AssetCalibrationQueryEditor({ query, onChange, onRunQuery, datas
setWorkspaces(workspaces);
}

setSystems(Array.from(datasource.systemAliasCache.values()));
getWorkspaces().catch(error => setError(parseErrorMessage(error) || 'Failed to fetch workspaces'));
}, [datasource]);
}, [datasource, areSystemsLoaded]);

const handleQueryChange = (value: AssetCalibrationQuery, runQuery: boolean): void => {
onChange(value);
Expand Down Expand Up @@ -69,21 +74,25 @@ export function AssetCalibrationQueryEditor({ query, onChange, onRunQuery, datas
}

return (
<div style={{ position: 'relative' }}>
<InlineField label="Group by" tooltip={tooltips.calibrationForecast.groupBy} labelWidth={22}>
<div className='asset-calibration-forecast'>
<InlineField shrink={true} style={{ maxWidth: '400px' }} label="Group by" tooltip={tooltips.calibrationForecast.groupBy} labelWidth={22}>
<MultiSelect
options={[...enumToOptions(AssetCalibrationTimeBasedGroupByType), ...enumToOptions(AssetCalibrationPropertyGroupByType)]}
onChange={handleGroupByChange}
width={85}
value={query.groupBy.map(toOption) || []}
/>
</InlineField>

<AssetCalibrationQueryBuilder
filter={query.filter}
workspaces={workspaces}
onChange={(event: any) => onParameterChange(event)}>
</AssetCalibrationQueryBuilder>
<div>
<Label>Filter</Label>

<AssetCalibrationQueryBuilder
filter={query.filter}
workspaces={workspaces}
systems={systems}
onChange={(event: any) => onParameterChange(event)}>
</AssetCalibrationQueryBuilder>
</div>

<FloatingError message={error} />
</div>
Expand Down
Loading

0 comments on commit 09950d0

Please sign in to comment.