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): add asset data source with utilization query #52

Closed
Closed
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
105 changes: 105 additions & 0 deletions src/core/errors.test.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for writing these tests!

Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { render } from '@testing-library/react'
import { FetchError } from '@grafana/runtime';
import { act } from 'react-dom/test-utils';
import { FloatingError, parseErrorMessage } from './errors';
import { SystemLinkError } from '../datasources/asset/types';
import React from 'react';

test('renders with error message', () => {
const { container } = render(<FloatingError message='error msg'/>)

expect(container.innerHTML).toBeTruthy() // refact: get by text
})

test('does not render without error message', () => {
const { container } = render(<FloatingError message=''/>)

expect(container.innerHTML).toBeFalsy() // refact: get by text
})

test('hides after timeout', () => {
jest.useFakeTimers();

const { container } = render(<FloatingError message='error msg'/>)
act(() => jest.runAllTimers())

expect(container.innerHTML).toBeFalsy()
})

test('parses error message', () => {
const errorMock: Error = {
name: 'error',
message: 'error message'
}

const result = parseErrorMessage(errorMock)

expect(result).toBe(errorMock.message)
})

test('parses fetch error message', () => {
const fetchErrorMock: FetchError = {
status: 404,
data: { message: 'error message' },
config: { url: 'URL' }
}

const result = parseErrorMessage(fetchErrorMock as any)

expect(result).toBe(fetchErrorMock.data.message)
})

test('parses fetch error status text', () => {
const fetchErrorMock: FetchError = {
status: 404,
data: {},
statusText: 'statusText',
config: { url: 'URL' }
}

const result = parseErrorMessage(fetchErrorMock as any)

expect(result).toBe(fetchErrorMock.statusText)
})

test('parses SystemLink error code', () => {
const systemLinkError: SystemLinkError = {
error: {
name: 'name',
args: [],
code: -255130,
message: 'error message'
}
}
const fetchErrorMock: FetchError = {
status: 404,
data: systemLinkError,
statusText: 'statusText',
config: { url: 'URL' }
}

const result = parseErrorMessage(fetchErrorMock as any)

expect(result).toBe(fetchErrorMock.statusText)
})

test('parses SystemLink error message', () => {
const systemLinkError: SystemLinkError = {
error: {
name: 'name',
args: [],
code: 123,
message: 'error message'
}
}
const fetchErrorMock: FetchError = {
status: 404,
data: systemLinkError,
statusText: 'statusText',
config: { url: 'URL' }
}

const result = parseErrorMessage(fetchErrorMock as any)

expect(result).toBe(fetchErrorMock.statusText)
})
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { isFetchError } from '@grafana/runtime';
import { Alert } from '@grafana/ui';
import { errorCodes } from './constants';
import { errorCodes } from '../datasources/data-frame/constants';
import React, { useState, useEffect } from 'react';
import { useTimeoutFn } from 'react-use';
import { isSystemLinkError } from './types';
import { isSystemLinkError } from './utils';

export const FloatingError = ({ message = '' }) => {
const [hide, setHide] = useState(false);
Expand All @@ -19,7 +19,7 @@ export const FloatingError = ({ message = '' }) => {
return <Alert title={message} elevated style={{ position: 'absolute', top: 0, right: 0, width: '50%' }} />;
};

export const parseErrorMessage = (error: Error) => {
export const parseErrorMessage = (error: Error): string | undefined => {
if (isFetchError(error)) {
if (isSystemLinkError(error.data)) {
return errorCodes[error.data.error.code] ?? error.data.error.message;
Expand Down
13 changes: 13 additions & 0 deletions src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,16 @@ export function replaceVariables(values: string[], templateSrv: TemplateSrv) {
// Dedupe and flatten
return [...new Set(replaced.flat())];
}

export interface SystemLinkError {
error: {
args: string[];
code: number;
message: string;
name: string;
}
}

export function isSystemLinkError(error: any): error is SystemLinkError {
return Boolean(error?.error?.code) && Boolean(error?.error?.name);
}
115 changes: 115 additions & 0 deletions src/datasources/asset/AssetDataSource.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { BackendSrv } from "@grafana/runtime";
import { MockProxy } from "jest-mock-extended";
import {
createFetchError,
createFetchResponse,
getQueryBuilder,
mockTimers,
peakDaysMock,
requestMatching,
setupDataSource,
assetModelMock,
} from "test/fixtures";
import { AssetDataSource } from "./AssetDataSource";
import {
AssetsResponse,
AssetQueryType,
AssetQuery,
EntityType,
IsNIAsset,
IsPeak,
UtilizationCategory,
TimeFrequency, PolicyOption
} from "./types";
import { dateTime } from "@grafana/data";


let ds: AssetDataSource, backendSrv: MockProxy<BackendSrv>

beforeEach(() => {
[ds, backendSrv] = setupDataSource(AssetDataSource);
});

const assetUtilizationQueryMock: AssetQuery = {
assetQueryType: AssetQueryType.METADATA,
workspace: '',
entityType: EntityType.ASSET,
isPeak: IsPeak.NONPEAK,
peakDays: peakDaysMock,
refId: '',
utilizationCategory: UtilizationCategory.TEST,
assetIdentifier: '321',
isNIAsset: IsNIAsset.NIASSET,
minionId: '123',
timeFrequency: TimeFrequency.DAILY,
peakStart: dateTime(new Date(2024, 1, 1, 9, 0)),
nonPeakStart: dateTime(new Date(2024, 1, 1, 17, 0)),
policyOption: PolicyOption.DEFAULT
}

const dataFrameDTOMock = [
{ name: 'model name', values: [''] },
{ name: 'serial number', values: [''] },
{ name: 'bus type', values: ['USB'] },
{ name: 'asset type', values: ['DEVICE_UNDER_TEST'] },
{ name: 'is NI asset', values: [true] },
{
name: 'calibration status',
values: ['APPROACHING_RECOMMENDED_DUE_DATE']
},
{ name: 'is system controller', values: [true] },
{ name: 'workspace', values: [''] },
{ name: 'last updated timestamp', values: [''] },
{ name: 'minionId', values: ['minion1'] },
{ name: 'parent name', values: [''] },
{ name: 'system name', values: ['system1'] },
{
name: 'calibration due date',
values: ['2019-05-07T18:58:05.000Z']
}
]

const buildQuery = getQueryBuilder<AssetQuery>()({});

mockTimers();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't necessary—you only need this if you're testing code that relies on window.setTimeout


describe('testDatasource', () => {
test('returns success', async () => {
backendSrv.fetch
.calledWith(requestMatching({ url: '/niapm/v1/assets' }))
.mockReturnValue(createFetchResponse(25));

const result = await ds.testDatasource();

expect(result.status).toEqual('success');
});

test('bubbles up exception', async () => {
backendSrv.fetch
.calledWith(requestMatching({ url: '/niapm/v1/assets' }))
.mockReturnValue(createFetchError(400));

await expect(ds.testDatasource()).rejects.toHaveProperty('status', 400);
});
})

describe('queries', () => {
test('runs metadata query', async () => {
const queryAssets = backendSrv.fetch
.calledWith(requestMatching({ url: '/niapm/v1/query-assets' }))
.mockReturnValue(createFetchResponse({ assets: assetModelMock, totalCount: 0 } as AssetsResponse))

const result = await ds.query(buildQuery(assetUtilizationQueryMock))

expect(result.data[0].fields).toEqual(expect.arrayContaining(dataFrameDTOMock))
expect(queryAssets).toHaveBeenCalledTimes(1)
})

test('handles query error', async () => {
backendSrv.fetch
.calledWith(requestMatching({ url: '/niapm/v1/query-assets' }))
.mockReturnValue(createFetchError(418))

await expect(ds.query(buildQuery(assetUtilizationQueryMock))).rejects.toThrow()
})
})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have tests that verify the output of the utilization queries. There's a lot of logic in the data source for building up filters and parsing responses that needs coverage. You can use Jest's snapshot testing to make it really easy to assert on the output. That's what we do in the tag data source:

expect(result.data).toMatchSnapshot();

I would also want to see a test that verifies that we replace template variables in queries correctly.

Loading
Loading