Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(asset-calibration): added systems as dropdown values for location #76

Merged
merged 1 commit into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions src/core/query-builder.utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { expressionBuilderCallback, expressionReaderCallback, transformComputedFieldsQuery } from "./query-builder.utils"
import { expressionBuilderCallback, expressionReaderCallback, ExpressionTransformFunction, 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 +28,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
31 changes: 27 additions & 4 deletions src/core/query-builder.utils.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,40 @@
import { QueryBuilderCustomOperation } from "smart-webcomponents-react";
saradei-ni marked this conversation as resolved.
Show resolved Hide resolved
import { QueryBuilderOption } from "./types";
import TTLCache from "@isaacs/ttlcache";

/**
* Should be used when looking to build a custom expression for a field
* @param value The value to be transformed
* @param operation The operation to be performed
* @param options The options to be used in the transformation
* @returns The transformed value
*/
export type ExpressionTransformFunction = (value: string, operation: string, options?: TTLCache<string, unknown>) => string;

/**
* Supported operations for computed fields
*/
export const computedFieldsupportedOperations = ['=', '!='];

/**
* 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 regex = new RegExp(`\\b${field}\\s*(${computedFieldsupportedOperations.join('|')})\\s*"([^"]*)"`, 'g');

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

return query;
Expand Down
35 changes: 30 additions & 5 deletions src/datasources/asset-calibration/AssetCalibrationDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,27 @@ import {
ColumnDescriptorType,
FieldDTOWithDescriptor,
} from './types';
import { transformComputedFieldsQuery } from 'core/query-builder.utils';
import { AssetComputedDataFields } from './constants';
import { ExpressionTransformFunction, transformComputedFieldsQuery } from 'core/query-builder.utils';
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 = {
groupBy: [],
filter: ''
};

public areSystemsLoaded = false;

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

private readonly baseUrl = this.instanceSettings.url + '/niapm/v1';

constructor(
readonly instanceSettings: DataSourceInstanceSettings,
readonly backendSrv: BackendSrv = getBackendSrv(),
Expand All @@ -37,15 +44,32 @@ export class AssetCalibrationDataSource extends DataSourceBase<AssetCalibrationQ
super(instanceSettings, backendSrv, templateSrv);
}

baseUrl = this.instanceSettings.url + '/niapm/v1';

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>) => {
saradei-ni marked this conversation as resolved.
Show resolved Hide resolved
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 Expand Up @@ -178,6 +202,7 @@ export class AssetCalibrationDataSource extends DataSourceBase<AssetCalibrationQ
}

const systems = await this.querySystems('', ['id', 'alias','connected.data.state', 'workspace']);
this.areSystemsLoaded = true;
systems.forEach(system => this.systemAliasCache.set(system.id, system));
}

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(), areDependenciesLoaded: true });
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
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { QueryBuilder, QueryBuilderCustomOperation, QueryBuilderProps } from 'smart-webcomponents-react/querybuilder';
import { useTheme2 } from '@grafana/ui';

Expand All @@ -13,27 +13,59 @@ 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[],
areDependenciesLoaded: boolean;
};

export const AssetCalibrationQueryBuilder: React.FC<AssetCalibrationQueryBuilderProps> = ({ filter, onChange, workspaces }) => {
export const AssetCalibrationQueryBuilder: React.FC<AssetCalibrationQueryBuilderProps> = ({ filter, onChange, workspaces, systems, areDependenciesLoaded }) => {
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) {
const workspaceField = getWorkspaceField(workspaces);
const workspaceField = useMemo(() => {
const workspaceField = AssetCalibrationFields.WORKSPACE;

return {
...workspaceField,
lookup: {
...workspaceField.lookup,
dataSource: [
...workspaceField.lookup?.dataSource || [],
...workspaces.map(({ id, name }) => ({ label: name, value: id }))
]
}
};
}, [workspaces]);

const locationField = useMemo(() => {
const locationField = AssetCalibrationFields.LOCATION;

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

useEffect(() => {
if (areDependenciesLoaded) {
const fields = [
...AssetCalibrationStaticFields,
workspaceField,
locationField,
...AssetCalibrationStaticFields,
];

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

return (
<QueryBuilder
Expand All @@ -77,20 +109,3 @@ export const AssetCalibrationQueryBuilder: React.FC<AssetCalibrationQueryBuilder
/>
);
};

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 }))
]
}
};
}


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;
}
Loading
Loading