Skip to content

Commit

Permalink
feat(asset): add the query type and group by buttons for calibration …
Browse files Browse the repository at this point in the history
…forecast (#64)

Co-authored-by: rmilea <robert.milea@ni.com>
Co-authored-by: vpstoynova <100705307+vpstoynova@users.noreply.github.com>
  • Loading branch information
3 people authored Aug 28, 2024
1 parent fe929af commit 7ab470b
Show file tree
Hide file tree
Showing 10 changed files with 798 additions and 322 deletions.
435 changes: 247 additions & 188 deletions src/datasources/asset/AssetDataSource.test.ts

Large diffs are not rendered by default.

92 changes: 88 additions & 4 deletions src/datasources/asset/AssetDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@ import {
DataFrameDTO,
DataQueryRequest,
DataSourceInstanceSettings,
FieldDTO,
TestDataSourceResponse,
} from '@grafana/data';
import { BackendSrv, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import { DataSourceBase } from 'core/DataSourceBase';
import {
AssetCalibrationForecastGroupByType,
AssetCalibrationForecastKey,
AssetCalibrationForecastQuery,
AssetFilterProperties,
AssetModel, AssetQuery,
AssetMetadataQuery,
AssetModel,
AssetQuery,
AssetQueryType,
AssetsResponse,
CalibrationForecastResponse,
} from './types';
import { getWorkspaceName, replaceVariables } from "../../core/utils";
import { SystemMetadata } from "../system/types";
Expand All @@ -28,16 +35,24 @@ export class AssetDataSource extends DataSourceBase<AssetQuery> {
baseUrl = this.instanceSettings.url + '/niapm/v1';

defaultQuery = {
type: AssetQueryType.Metadata,
queryKind: AssetQueryType.Metadata,
workspace: '',
minionIds: [],
groupBy: []
};

async runQuery(query: AssetQuery, options: DataQueryRequest): Promise<DataFrameDTO> {
return await this.processMetadataQuery(query)
switch (query.queryKind) {
case AssetQueryType.Metadata:
return await this.processMetadataQuery(query as AssetMetadataQuery);
case AssetQueryType.CalibrationForecast:
return await this.processCalibrationForecastQuery(query as AssetCalibrationForecastQuery, options);
default:
throw new Error(`Unknown query type: ${query.queryKind}`);
}
}

async processMetadataQuery(query: AssetQuery) {
async processMetadataQuery(query: AssetMetadataQuery) {
const result: DataFrameDTO = { refId: query.refId, fields: [] };
const minionIds = replaceVariables(query.minionIds, this.templateSrv);
let workspaceId = this.templateSrv.replace(query.workspace);
Expand Down Expand Up @@ -70,6 +85,65 @@ export class AssetDataSource extends DataSourceBase<AssetQuery> {
return result;
}

async processCalibrationForecastQuery(query: AssetCalibrationForecastQuery, 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);

result.fields = calibrationForecastResponse.calibrationForecast.columns || [];
result.fields = result.fields.map(field => this.formatField(field, query));

return result;
}

formatField(field: FieldDTO, query: AssetCalibrationForecastQuery): FieldDTO {
if (!field.values) {
return field;
}

if (field.name === AssetCalibrationForecastKey.Time) {
field.values = this.formatTimeField(field.values, query);
field.name= 'Formatted Time';
return field;
}

return field;
}

formatTimeField(values: string[], query: AssetCalibrationForecastQuery): string[] {
const timeGrouping = query.groupBy.find(item =>
[AssetCalibrationForecastGroupByType.Day,
AssetCalibrationForecastGroupByType.Week,
AssetCalibrationForecastGroupByType.Month].includes(item as AssetCalibrationForecastGroupByType)
) as AssetCalibrationForecastGroupByType | undefined;

const formatFunctionMap = {
[AssetCalibrationForecastGroupByType.Day]: this.formatDateForDay,
[AssetCalibrationForecastGroupByType.Week]: this.formatDateForWeek,
[AssetCalibrationForecastGroupByType.Month]: this.formatDateForMonth,
};

const formatFunction = formatFunctionMap[timeGrouping!] || ((v: string) => v);
values = values.map(formatFunction);
return values;
}

formatDateForDay(date: string): string {
return new Date(date).toISOString().split('T')[0];
}

formatDateForWeek(date: string): string {
const startDate = new Date(date);
const endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 6);
return `${startDate.toISOString().split('T')[0]} : ${endDate.toISOString().split('T')[0]}`;
}

formatDateForMonth(date: string): string {
return new Date(date).toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
}

shouldRunQuery(_: AssetQuery): boolean {
return true;
Expand All @@ -85,6 +159,16 @@ export class AssetDataSource extends DataSourceBase<AssetQuery> {
}
}

async queryCalibrationForecast(groupBy: string[], startTime: string, endTime: string, filter = ''): Promise<CalibrationForecastResponse> {
let data = { groupBy, startTime, endTime, filter };
try {
let response = await this.post<CalibrationForecastResponse>(this.baseUrl + '/assets/calibration-forecast', data);
return response;
} catch (error) {
throw new Error(`An error occurred while querying assets calibration forecast: ${error}`);
}
}

async querySystems(filter = '', projection = defaultProjection): Promise<SystemMetadata[]> {
try {
let response = await this.getSystems({
Expand Down
43 changes: 43 additions & 0 deletions src/datasources/asset/__snapshots__/AssetDataSource.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`queries run calibration forecast query 1`] = `
[
{
"fields": [
{
"name": "Formatted Time",
"values": [
"July 2024",
"August 2024",
"September 2024",
"October 2024",
"November 2024",
"December 2024",
],
},
{
"name": "Lab1",
"values": [
1,
2,
3,
4,
5,
6,
],
},
{
"name": "Lab2",
"values": [
0,
1,
2,
3,
4,
4,
],
},
],
"refId": "A",
},
]
`;

exports[`queries run metadata query 1`] = `
[
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { screen, waitFor } from '@testing-library/react';
import { setupRenderer } from '../../../test/fixtures';
import { SystemMetadata } from '../../system/types';
import { AssetDataSource } from '../AssetDataSource';
import { AssetQueryEditor } from './AssetQueryEditor';
import { AssetCalibrationForecastGroupByType, AssetCalibrationForecastQuery, AssetQueryType } from '../types';
import { select } from 'react-select-event';

const fakeSystems: SystemMetadata[] = [
{
id: '1',
state: 'CONNECTED',
workspace: '1',
},
{
id: '2',
state: 'CONNECTED',
workspace: '2',
},
];

class FakeAssetDataSource extends AssetDataSource {
querySystems(filter?: string, projection?: string[]): Promise<SystemMetadata[]> {
return Promise.resolve(fakeSystems);
}
}

const render = setupRenderer(AssetQueryEditor, FakeAssetDataSource);

it('renders with query type calibration forecast', async () => {
render({ queryKind: AssetQueryType.CalibrationForecast } as AssetCalibrationForecastQuery);

const groupBy = screen.getAllByRole('combobox')[1];
expect(groupBy).not.toBeNull();
});

it('renders with query type calibration forecast and updates group by', async () => {
const [onChange] = render({
queryKind: AssetQueryType.CalibrationForecast,
groupBy: [AssetCalibrationForecastGroupByType.Month],
} as AssetCalibrationForecastQuery);

// User selects group by day
const groupBy = screen.getAllByRole('combobox')[1];
await select(groupBy, "Day", { container: document.body });
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({ groupBy: [AssetCalibrationForecastGroupByType.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: [AssetCalibrationForecastGroupByType.Week],
})
);
});

// User selects group by location and month, overrides time
await select(groupBy, "Month", { container: document.body });
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
groupBy: [AssetCalibrationForecastGroupByType.Month],
})
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { SelectableValue, toOption } from '@grafana/data';
import { AssetDataSource } from '../AssetDataSource';
import { AssetCalibrationForecastGroupByType, AssetCalibrationForecastQuery, AssetQuery } from '../types';
import { InlineField, MultiSelect } from '@grafana/ui';
import React from 'react';
import { enumToOptions } from '../../../core/utils';
import _ from 'lodash';

type Props = {
query: AssetCalibrationForecastQuery;
handleQueryChange: (value: AssetQuery, runQuery: boolean) => void;
datasource: AssetDataSource;
};

export function QueryCalibrationForecastEditor({ query, handleQueryChange, datasource }: Props) {
query = datasource.prepareQuery(query) as AssetCalibrationForecastQuery;
const handleGroupByChange = (items?: Array<SelectableValue<string>>): void => {
if (!items || _.isEqual(query.groupBy, items)) {
return;
}

let groupBy: string[] = [];
let timeGrouping: string = null!;

for (let item of items) {
if (item.value === AssetCalibrationForecastGroupByType.Day || item.value === AssetCalibrationForecastGroupByType.Week || item.value === AssetCalibrationForecastGroupByType.Month) {
timeGrouping = item.value;
continue;
}

groupBy.push(item.value!);
}

if (timeGrouping) {
groupBy.push(timeGrouping);
}
groupBy = groupBy.slice(-2);

if (!_.isEqual(query.groupBy, groupBy)) {
handleQueryChange({ ...query, groupBy: groupBy }, groupBy.length !== 0);
}
};

return (
<>
<InlineField label="Group by" tooltip={tooltips.calibrationForecast.groupBy} labelWidth={22}>
<MultiSelect
options={enumToOptions(AssetCalibrationForecastGroupByType)}
onChange={handleGroupByChange}
width={85}
value={query.groupBy.map(toOption) || []}
/>
</InlineField>
</>
);
}

const tooltips = {
calibrationForecast: {
groupBy: `Group the calibration forecast by day, week, month. Only one time-based grouping is allowed. Only two groupings are allowed. This is a required field.`,
},
};
27 changes: 20 additions & 7 deletions src/datasources/asset/components/AssetQueryEditor.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AssetDataSource } from "../AssetDataSource"
import { setupRenderer } from "test/fixtures"
import { AssetQuery, AssetQueryType } from "../types"
import { AssetCalibrationForecastGroupByType, AssetCalibrationForecastQuery, AssetMetadataQuery, AssetQuery, AssetQueryType } from "../types"
import { screen, waitFor, waitForElementToBeRemoved } from "@testing-library/react"
import { AssetQueryEditor } from "./AssetQueryEditor"
import { select } from "react-select-event";
Expand Down Expand Up @@ -32,20 +32,22 @@ it('renders with query defaults', async () => {
render({} as AssetQuery)
await workspacesLoaded()

expect(screen.getAllByRole('combobox')[0]).toHaveAccessibleDescription('Any workspace');
expect(screen.getAllByRole('combobox')[1]).toHaveAccessibleDescription('Select systems');
expect(screen.getAllByRole('combobox')[0]).toHaveAccessibleDescription('');
expect(screen.queryByLabelText('Group by')).not.toBeInTheDocument();
expect(screen.getAllByRole('combobox')[1]).toHaveAccessibleDescription('Any workspace');
expect(screen.getAllByRole('combobox')[2]).toHaveAccessibleDescription('Select systems');
})

it('renders with initial query and updates when user makes changes', async () => {
const [onChange] = render({ type: AssetQueryType.Metadata, minionIds: ['1'], workspace: '2' });
const [onChange] = render({ queryKind: AssetQueryType.Metadata, minionIds: ['1'], workspace: '2' } as AssetMetadataQuery);
await workspacesLoaded();

// Renders saved query
expect(screen.getByText('Other workspace')).toBeInTheDocument();
expect(screen.getByText('1')).toBeInTheDocument();

// User selects different workspace
await select(screen.getAllByRole('combobox')[0], 'Default workspace', { container: document.body });
await select(screen.getAllByRole('combobox')[1], 'Default workspace', { container: document.body });
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ workspace: '1' }));
});
Expand All @@ -56,14 +58,25 @@ it('renders with initial query and updates when user makes changes', async () =>
});

// User selects system
await select(screen.getAllByRole('combobox')[1], '2', { container: document.body });
await select(screen.getAllByRole('combobox')[2], '2', { container: document.body });
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ minionIds: ['2'] }));
});

// User adds another system
await select(screen.getAllByRole('combobox')[1], '$test_var', { container: document.body });
await select(screen.getAllByRole('combobox')[2], '$test_var', { container: document.body });
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ minionIds: ['2', '$test_var'] }));
});
});


it('renders with query type calibration forecast and updates when user makes changes', async () => {
const [onChange] = render({ queryKind: AssetQueryType.CalibrationForecast, groupBy: [AssetCalibrationForecastGroupByType.Month] } as AssetCalibrationForecastQuery);

// User selects group by
await select(screen.getAllByRole('combobox')[1], "Day" , { container: document.body });
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ groupBy: [AssetCalibrationForecastGroupByType.Day] }));
});
});
Loading

0 comments on commit 7ab470b

Please sign in to comment.