=> ({
+ 'label': asset.name,
+ 'value': asset.id,
+ description: asset.id
+ })
+ );
+ options.unshift(...getVariableOptions());
+
+ return options;
+ }
+
+ return (
+
+
+ handleAssetQueryTypeChange(item)}
+ />
+
+ {query.assetQueryType === AssetQueryType.METADATA &&
+ (<>
+
+
+
+
+
+
+ >)}
+ {
+ query.assetQueryType === AssetQueryType.UTILIZATION &&
+ (<>
+
+ handleEntityTypeChange(item)}
+ id={'ss'}
+ />
+
+
+
+
+
+
+
+
+ handleIsNIAssetChange(item)}
+ />
+
+ {query.entityType === EntityType.ASSET && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ return peakDayOptions[day]
+ })}
+ onChange={(item: any) => handlePeakDaysChange(item)}
+ />
+
+ >)
+ }
+
+
+ );
+}
diff --git a/src/datasources/asset/components/AssetVariableQueryEditor.tsx b/src/datasources/asset/components/AssetVariableQueryEditor.tsx
new file mode 100644
index 0000000..a12fd93
--- /dev/null
+++ b/src/datasources/asset/components/AssetVariableQueryEditor.tsx
@@ -0,0 +1,22 @@
+import React, { FormEvent } from 'react';
+import { InlineField } from "../../../core/components/InlineField";
+import { AutoSizeInput } from "@grafana/ui";
+import { AssetVariableQuery } from "../types";
+
+interface Props {
+ query: AssetVariableQuery;
+ onChange: (query: AssetVariableQuery) => void;
+}
+
+export function AssetVariableQueryEditor({ onChange, query }: Props) {
+
+ return (
+
+ ) => onChange({ minionId: event.currentTarget.value ?? '' })}
+ placeholder="Any system"
+ defaultValue={query.minionId}
+ />
+
+ );
+}
diff --git a/src/datasources/asset/constants.ts b/src/datasources/asset/constants.ts
new file mode 100644
index 0000000..83ad1f7
--- /dev/null
+++ b/src/datasources/asset/constants.ts
@@ -0,0 +1,192 @@
+import {
+ AssetQueryType,
+ EntityType,
+ IsNIAsset,
+ IsPeak,
+ PolicyOption,
+ TimeFrequency,
+ UtilizationCategory,
+ Weekday
+} from "./types";
+import { SelectableValue } from "@grafana/data";
+
+export const assetQueryTypeLabels: { [key in AssetQueryType]: string } = {
+ [AssetQueryType.METADATA]: 'Metadata',
+ [AssetQueryType.UTILIZATION]: 'Utilization',
+}
+
+export const entityTypeLabels: { [key in EntityType]: string } = {
+ [EntityType.ASSET]: 'Asset',
+ [EntityType.SYSTEM]: 'System',
+}
+
+export const isPeakLabels: { [key in IsPeak]: string } = {
+ [IsPeak.PEAK]: 'Peak',
+ [IsPeak.NONPEAK]: 'Non-Peak',
+}
+
+export const peakDayLabels: { [key in Weekday]: string } = {
+ [Weekday.Sunday]: 'Sunday',
+ [Weekday.Monday]: 'Monday',
+ [Weekday.Tuesday]: 'Tuesday',
+ [Weekday.Wednesday]: 'Wednesday',
+ [Weekday.Thursday]: 'Thursday',
+ [Weekday.Friday]: 'Friday',
+ [Weekday.Saturday]: 'Saturday',
+}
+
+export const utilizationCategoryLabels: { [key in UtilizationCategory]: string } = {
+ [UtilizationCategory.ALL]: 'All',
+ [UtilizationCategory.TEST]: 'Test',
+}
+
+export const timeFrequencyLabels: { [key in TimeFrequency]: string } = {
+ [TimeFrequency.DAILY]: 'Daily',
+ [TimeFrequency.HOURLY]: 'Hourly',
+}
+
+export const assetQueryTypeOptions: SelectableValue[] = [
+ {
+ value: AssetQueryType.METADATA,
+ label: assetQueryTypeLabels[AssetQueryType.METADATA],
+ description: assetQueryTypeLabels[AssetQueryType.METADATA],
+ },
+ {
+ value: AssetQueryType.UTILIZATION,
+ label: assetQueryTypeLabels[AssetQueryType.UTILIZATION],
+ description: assetQueryTypeLabels[AssetQueryType.UTILIZATION],
+ }
+]
+
+export const entityTypeOptions: SelectableValue[] = [
+ {
+ value: EntityType.ASSET,
+ label: entityTypeLabels[EntityType.ASSET],
+ description: entityTypeLabels[EntityType.ASSET],
+ },
+ {
+ value: EntityType.SYSTEM,
+ label: entityTypeLabels[EntityType.SYSTEM],
+ description: entityTypeLabels[EntityType.SYSTEM],
+ }
+]
+
+export const isPeakOptions: SelectableValue[] = [
+ {
+ value: IsPeak.PEAK,
+ label: isPeakLabels[IsPeak.PEAK],
+ description: `Peak`,
+ },
+ {
+ value: IsPeak.NONPEAK,
+ label: isPeakLabels[IsPeak.NONPEAK],
+ description: `Non-Peak`,
+ }
+]
+
+export const policyOptions: SelectableValue[] = [
+ {
+ value: PolicyOption.DEFAULT,
+ label: "Default",
+ },
+ {
+ value: PolicyOption.ALL,
+ label: "All day",
+ },
+ {
+ value: PolicyOption.CUSTOM,
+ label: "Custom",
+ }
+]
+
+export const peakDayOptions: SelectableValue[] = [
+ {
+ value: Weekday.Sunday,
+ label: peakDayLabels[Weekday.Sunday],
+ description: `${peakDayLabels[Weekday.Sunday]}`,
+ },
+ {
+ value: Weekday.Monday,
+ label: peakDayLabels[Weekday.Monday],
+ description: `${peakDayLabels[Weekday.Monday]}`,
+ },
+ {
+ value: Weekday.Tuesday,
+ label: peakDayLabels[Weekday.Tuesday],
+ description: `${peakDayLabels[Weekday.Tuesday]}`,
+ },
+ {
+ value: Weekday.Wednesday,
+ label: peakDayLabels[Weekday.Wednesday],
+ description: `${peakDayLabels[Weekday.Wednesday]}`,
+ },
+ {
+ value: Weekday.Thursday,
+ label: peakDayLabels[Weekday.Thursday],
+ description: `${peakDayLabels[Weekday.Thursday]}`,
+ },
+ {
+ value: Weekday.Friday,
+ label: peakDayLabels[Weekday.Friday],
+ description: `${peakDayLabels[Weekday.Friday]}`,
+ },
+ {
+ value: Weekday.Saturday,
+ label: peakDayLabels[Weekday.Saturday],
+ description: `${peakDayLabels[Weekday.Saturday]}`,
+ },
+]
+
+export const utilizationCategoryOptions: SelectableValue[] = [
+ {
+ value: UtilizationCategory.ALL,
+ label: utilizationCategoryLabels[UtilizationCategory.ALL],
+ description: `All`,
+ },
+ {
+ value: UtilizationCategory.TEST,
+ label: utilizationCategoryLabels[UtilizationCategory.TEST],
+ description: `Test`,
+ }
+]
+
+export const timeFrequencyOptions: SelectableValue[] = [
+ {
+ value: TimeFrequency.DAILY,
+ label: timeFrequencyLabels[TimeFrequency.DAILY],
+ description: `${timeFrequencyLabels[TimeFrequency.DAILY]}`,
+ },
+ {
+ value: TimeFrequency.HOURLY,
+ label: timeFrequencyLabels[TimeFrequency.HOURLY],
+ description: `${timeFrequencyLabels[TimeFrequency.HOURLY]}`,
+ }
+]
+
+export const isNIAssetLabel: { [key in IsNIAsset]: string } = {
+ [IsNIAsset.BOTH]: 'All',
+ [IsNIAsset.NIASSET]: 'NI',
+ [IsNIAsset.NOTNIASSET]: 'Non-NI',
+}
+
+export const isNIAssetOptions = [
+ {
+ value: IsNIAsset.BOTH,
+ label: isNIAssetLabel[IsNIAsset.BOTH],
+ description: `Select all`,
+ },
+ {
+ value: IsNIAsset.NIASSET,
+ label: isNIAssetLabel[IsNIAsset.NIASSET],
+ description: `Select NI`,
+ },
+ {
+ value: IsNIAsset.NOTNIASSET,
+ label: isNIAssetLabel[IsNIAsset.NOTNIASSET],
+ description: `Select not NI`,
+ }
+]
+
+export const minuteInSeconds = 60 * 1000;
+export const hourInSeconds = 60 * minuteInSeconds
+export const secondsInDay = 24 * hourInSeconds
diff --git a/src/datasources/asset/helper.test.ts b/src/datasources/asset/helper.test.ts
new file mode 100644
index 0000000..5ba78b7
--- /dev/null
+++ b/src/datasources/asset/helper.test.ts
@@ -0,0 +1,297 @@
+import {
+ AssetUtilizationHistoryMock,
+ endAMock,
+ endBMock,
+ intervalAMock,
+ intervalBMock,
+ peakDaysMock,
+ startAMock,
+ startBMock
+} from 'test/fixtures';
+import {
+ arraysEqual,
+ calculateUtilization,
+ calculatePeakMilliseconds,
+ divideTimeRangeToBusinessIntervals,
+ extractTimestampsFromData,
+ groupDataByIntervals,
+ mergeOverlappingIntervals,
+ patchMissingEndTimestamps,
+} from './helper';
+import { IntervalsWithPeakFlag, Interval, IntervalWithHeartbeat, IsPeak, TimeFrequency, Weekday } from './types';
+import { minuteInSeconds } from "./constants";
+
+afterEach(() => {
+ jest.resetAllMocks(); /* reset because of timezone mock*/
+})
+
+test('calculates peak milliseconds', () => {
+ const intervalMock: IntervalsWithPeakFlag = {
+ startTimestamp: new Date('2023-11-20:00:00:00Z'),
+ endTimestamp: new Date('2023-11-20:01:00:00Z'),
+ isWorking: true
+ }
+
+ const result = calculatePeakMilliseconds(intervalMock)
+
+ expect(result).toBe(3600000)
+})
+
+test('extracts timestamps from data', () => {
+ const result = extractTimestampsFromData(AssetUtilizationHistoryMock)
+
+ expect(result[0].startTimestamp).toBe(new Date(AssetUtilizationHistoryMock[0].startTimestamp).getTime())
+ expect(result[0].endTimestamp).toBe(new Date(AssetUtilizationHistoryMock[0].endTimestamp!).getTime())
+ expect(result[0].heartbeatTimestamp).toBe(new Date(AssetUtilizationHistoryMock[0].heartbeatTimestamp!).getTime())
+ expect(result[1].startTimestamp).toBe(new Date(AssetUtilizationHistoryMock[1].startTimestamp).getTime())
+ expect(result[1].endTimestamp).toBe(0)
+ expect(result[1].heartbeatTimestamp).toBe(0)
+})
+
+describe('patchMissingEndTimestamps', () => {
+ test('patches missing end timestamps with start and heartbeat timestamps', () => {
+ const historyMock: Array> = [
+ {
+ startTimestamp: new Date('2023-11-20:00:00:00Z').getTime(),
+ endTimestamp: new Date('2023-11-20:02:00:00Z').getTime(),
+ heartbeatTimestamp: new Date('2023-11-20:01:00:00Z').getTime(),
+ },
+ {
+ startTimestamp: new Date('2023-11-21:00:00:00Z').getTime(),
+ endTimestamp: 0,
+ heartbeatTimestamp: new Date('2023-11-21:01:00:00Z').getTime(),
+ },
+ {
+ startTimestamp: new Date('2023-11-22:00:00:00Z').getTime(),
+ endTimestamp: 0,
+ heartbeatTimestamp: 0,
+ }
+ ]
+
+ const result = patchMissingEndTimestamps(historyMock)
+
+ expect(result[0].endTimestamp).toBe(historyMock[0].endTimestamp)
+ expect(result[1].endTimestamp).toBe(historyMock[1].heartbeatTimestamp)
+ expect(result[2].endTimestamp).toBe(historyMock[2].startTimestamp + 10 * minuteInSeconds)
+ })
+})
+
+describe('mergeOverlappingIntervals', () => {
+ test('merges overlapping intervals', () => {
+ const intervalsMock: Array> = [
+ {
+ startTimestamp: startAMock.getTime(),
+ endTimestamp: endAMock.getTime()
+ },
+ {
+ startTimestamp: startBMock.getTime(),
+ endTimestamp: endBMock.getTime()
+ }
+ ]
+
+ const result = mergeOverlappingIntervals(intervalsMock)
+
+ expect(result[0].startTimestamp).toBe(startAMock.getTime())
+ expect(result[0].endTimestamp).toBe(endBMock.getTime())
+ })
+
+ test('does not merge non-overlapping interval', () => {
+ const intervalsMock: Array> = [
+ {
+ startTimestamp: new Date('2023-11-20:03:00:00Z').getTime(),
+ endTimestamp: new Date('2023-11-20:05:00:00Z').getTime()
+ },
+ {
+ startTimestamp: startAMock.getTime(),
+ endTimestamp: endAMock.getTime()
+ }
+ ]
+
+ const result = mergeOverlappingIntervals(intervalsMock)
+
+ expect(result).toStrictEqual(intervalsMock)
+ })
+
+ test('returns original single item array', () => {
+ const intervalsMock: Array> = [
+ {
+ startTimestamp: startAMock.getTime(),
+ endTimestamp: endAMock.getTime()
+ }
+ ]
+
+ const result = mergeOverlappingIntervals(intervalsMock)
+
+ expect(result).toStrictEqual(intervalsMock)
+ })
+})
+
+describe('divideTimeRangeToBusinessIntervals', () => {
+
+ it('should handle non-peak days as non-working intervals', () => {
+ const rangeStart = new Date('2024-03-16T08:00:00Z'); // Monday
+ const rangeEnd = new Date('2024-03-17T18:00:00Z');
+ const workingHours = { startTime: '09:00', endTime: '17:00' };
+ const peakDays = [Weekday.Saturday, Weekday.Sunday]; // Non-peak days are weekdays here
+ const timeFrequency = TimeFrequency.DAILY;
+
+ const intervals = divideTimeRangeToBusinessIntervals(rangeStart, rangeEnd, workingHours, peakDays, timeFrequency);
+ expect(intervals).toHaveLength(5); // Since it's a non-peak weekday, the whole interval is marked as non-working
+ expect(intervals).toStrictEqual([
+ {
+ "endTimestamp": new Date("2024-03-16T09:00:00.000Z"),
+ "isWorking": false,
+ "startTimestamp": new Date("2024-03-16T08:00:00.000Z")
+ },
+ {
+ "endTimestamp": new Date("2024-03-16T17:00:00.000Z"),
+ "isWorking": true,
+ "startTimestamp": new Date("2024-03-16T09:00:00.000Z")
+ },
+ {
+ "endTimestamp": new Date("2024-03-17T09:00:00.000Z"),
+ "isWorking": false,
+ "startTimestamp": new Date("2024-03-16T17:00:00.000Z")
+ },
+ {
+ "endTimestamp": new Date("2024-03-17T17:00:00.000Z"),
+ "isWorking": true,
+ "startTimestamp": new Date("2024-03-17T09:00:00.000Z")
+ },
+ {
+ "endTimestamp": new Date("2024-03-17T18:00:00.000Z"),
+ "isWorking": false,
+ "startTimestamp": new Date("2024-03-17T17:00:00.000Z")
+ }
+ ])
+ });
+
+ it('should split intervals correctly for hourly frequency across a working and non-working boundary', () => {
+ const rangeStart = new Date('2024-03-15T08:00:00Z'); // Friday
+ const rangeEnd = new Date('2024-03-15T10:00:00Z');
+ const workingHours = { startTime: '09:00', endTime: '17:00' };
+ const peakDays = [Weekday.Monday, Weekday.Tuesday, Weekday.Wednesday, Weekday.Thursday, Weekday.Friday];
+ const timeFrequency = TimeFrequency.HOURLY;
+
+ const intervals = divideTimeRangeToBusinessIntervals(rangeStart, rangeEnd, workingHours, peakDays, timeFrequency);
+ expect(intervals).toHaveLength(2); // Should be split into one non-working hour and one working hour
+ expect(intervals[0].isWorking).toBeFalsy();
+ expect(intervals[1].isWorking).toBeTruthy();
+ });
+})
+
+describe('calculateUtilization', () => {
+ test('calculates daily utilization with peak seconds 1', () => {
+ const dayMock = new Date('2023-11-20')
+ const intervalsByDayMock: Array<{ day: Date, interval: IntervalsWithPeakFlag, overlapsWith: Date[][] }> = [
+ {
+ day: dayMock,
+ interval: {
+ startTimestamp: new Date('2024-03-11:00:00:00Z'),
+ endTimestamp: new Date('2024-03-11:02:00:00Z'),
+ isWorking: true
+ },
+ overlapsWith: [
+ [new Date('2024-03-11:00:00:00Z'), new Date('2024-03-11:01:00:00Z')]
+ ]
+ }
+ ]
+
+ const result = calculateUtilization(intervalsByDayMock)
+
+ expect(result.length).toBe(1)
+ expect(result[0].utilization).toBe(50)
+ })
+
+ test('calculates daily utilization with peak seconds 2', () => {
+ const dayMock = new Date('2024-03-11')
+ const timestampMock = new Date('2024-03-11:00:00:00Z')
+ const intervalsByDayMock: Array<{ day: Date, interval: IntervalsWithPeakFlag, overlapsWith: Date[][] }> = [
+ {
+ day: dayMock,
+ interval: {
+ startTimestamp: timestampMock,
+ endTimestamp: timestampMock,
+ isWorking: true
+ },
+ overlapsWith: []
+ }
+ ]
+
+ const result = calculateUtilization(intervalsByDayMock)
+
+ expect(result.length).toBe(1)
+ expect(result[0].utilization).toBe(0)
+ })
+})
+
+describe('groupDataByIntervals', () => {
+ test('splits non-peak intervals', () => {
+ const result = groupDataByIntervals(intervalAMock, intervalBMock, [], IsPeak.NONPEAK)
+
+ expect(result.length).toBe(0)
+ })
+
+ test('splits peak intervals without overlapping', () => {
+ const result = groupDataByIntervals(intervalAMock, intervalBMock, peakDaysMock, IsPeak.PEAK)
+
+ expect(result.length).toBe(1)
+ expect(result[0].overlapsWith).toStrictEqual([[startBMock, endAMock]])
+ expect(result[0].interval).toStrictEqual(
+ {
+ startTimestamp: startAMock,
+ endTimestamp: endAMock,
+ isWorking: true
+ })
+ })
+
+ test('splits non-peak intervals (reverse intervals)', () => {
+ const intervalAMock: Array> = [
+ {
+ startTimestamp: startBMock,
+ endTimestamp: endBMock,
+ isWorking: true
+ }
+ ]
+
+ const intervalBMock: Array> = [
+ {
+ startTimestamp: startAMock.getTime(),
+ endTimestamp: endAMock.getTime()
+ }
+ ]
+
+ const result = groupDataByIntervals(intervalAMock, intervalBMock, [], IsPeak.NONPEAK)
+
+ expect(result.length).toBe(0)
+ })
+})
+
+describe('arraysEqual', () => {
+ test('returns false with different size arrays', () => {
+ const array1 = [1, 2]
+ const array2 = [1, 2, 3]
+
+ const result = arraysEqual(array1, array2)
+
+ expect(result).toBeFalsy()
+ })
+
+ test('returns false with different array vlues', () => {
+ const array1 = [1, 2, 3]
+ const array2 = [1, 2, 5]
+
+ const result = arraysEqual(array1, array2)
+
+ expect(result).toBeFalsy()
+ })
+
+ test('returns true for identical arrays', () => {
+ const array1 = [1, 2, 3]
+ const array2 = [1, 2, 3]
+
+ const result = arraysEqual(array1, array2)
+
+ expect(result).toBeTruthy()
+ })
+})
diff --git a/src/datasources/asset/helper.ts b/src/datasources/asset/helper.ts
new file mode 100644
index 0000000..474d20f
--- /dev/null
+++ b/src/datasources/asset/helper.ts
@@ -0,0 +1,463 @@
+import {
+ AssetUtilizationHistory,
+ IntervalsWithPeakFlag,
+ Interval,
+ IntervalWithHeartbeat,
+ IsPeak,
+ TimeFrequency,
+ Weekday,
+} from "./types";
+import { minuteInSeconds } from "./constants";
+
+export const extractTimestampsFromData = (history: AssetUtilizationHistory[]): Array> => {
+ return history.map((item) => {
+ return {
+ 'startTimestamp': new Date(item.startTimestamp).getTime(),
+ 'endTimestamp': item.endTimestamp ? new Date(item.endTimestamp).getTime() : 0,
+ 'heartbeatTimestamp': item.heartbeatTimestamp ? new Date(item.heartbeatTimestamp).getTime() : 0
+ }
+ })
+}
+
+export const patchMissingEndTimestamps = (history: Array>): Array> => {
+ return history.map((item: IntervalWithHeartbeat) => {
+ let newItem: Interval = {
+ 'startTimestamp': new Date(item.startTimestamp).getTime(),
+ 'endTimestamp': 0
+ }
+ if (!item.endTimestamp) {
+ if (!item.heartbeatTimestamp) {
+ newItem.endTimestamp = new Date(item.startTimestamp).getTime() + 10 * minuteInSeconds
+ } else {
+ newItem.endTimestamp = new Date(item.heartbeatTimestamp).getTime()
+ }
+ } else {
+ newItem.endTimestamp = new Date(item.endTimestamp).getTime()
+ }
+
+ return newItem
+ })
+}
+
+export const filterDataByTimeRange = (data: Array>, from: number, to: number): Array> => {
+ return data.filter((interval: Interval) => {
+ return interval.endTimestamp > from && interval.startTimestamp < to;
+ })
+}
+
+export const mergeOverlappingIntervals = (intervals: Array>): Array> => {
+ if (intervals.length <= 1) {
+ return intervals;
+ }
+ const mergedIntervals = [intervals[0]];
+ for (let i = 1; i < intervals.length; i++) {
+ const currentInterval = intervals[i];
+ const previousInterval = mergedIntervals[mergedIntervals.length - 1];
+
+ if (currentInterval.startTimestamp <= previousInterval.endTimestamp) {
+ // Overlapping intervals, merge them
+ mergedIntervals[mergedIntervals.length - 1].endTimestamp = Math.max(
+ previousInterval.endTimestamp,
+ currentInterval.endTimestamp
+ );
+ } else {
+ // Non-overlapping intervals, add the current interval to the result
+ mergedIntervals.push(currentInterval);
+ }
+ }
+
+ return mergedIntervals;
+}
+
+export const groupDataByIntervals = (
+ businessIntervals: Array>,
+ utilizationIntervals: Array>,
+ peakDays: Weekday[],
+ isPeak: IsPeak
+): Array<{ day: Date, interval: IntervalsWithPeakFlag, overlapsWith: Date[][] }> => {
+ let overlaps = [];
+
+ for (let businessInterval of businessIntervals) {
+ let overlappingSegments = [];
+ for (let utilizationInterval of utilizationIntervals) {
+ let businessIntervalStart = new Date(businessInterval['startTimestamp']);
+ let businessIntervalEnd = new Date(businessInterval["endTimestamp"]);
+ let utilizationIntervalStart = new Date(utilizationInterval['startTimestamp']);
+ let utilizationIntervalEnd = new Date(utilizationInterval["endTimestamp"]);
+ // Check for overlap
+ if (businessIntervalStart < utilizationIntervalEnd && utilizationIntervalStart < businessIntervalEnd) {
+ // Overlapping interval found
+ let overlapStart = businessIntervalStart > utilizationIntervalStart ? businessIntervalStart : utilizationIntervalStart;
+ let overlapEnd = businessIntervalEnd < utilizationIntervalEnd ? businessIntervalEnd : utilizationIntervalEnd;
+ overlappingSegments.push([overlapStart, overlapEnd]);
+ }
+ }
+ if (isPeak === IsPeak.NONPEAK) {
+ if (!businessInterval.isWorking) {
+ overlaps.push({
+ day: businessInterval.startTimestamp,
+ interval: businessInterval,
+ overlapsWith: overlappingSegments
+ });
+ }
+ } else {
+ if (businessInterval.isWorking) {
+ overlaps.push({
+ day: businessInterval.startTimestamp,
+ interval: businessInterval,
+ overlapsWith: overlappingSegments
+ });
+ }
+ }
+ }
+
+ return overlaps;
+}
+
+export const calculatePeakMilliseconds = (businessHours: IntervalsWithPeakFlag): number => {
+ let { startTimestamp, endTimestamp } = businessHours
+ return endTimestamp.getTime() - startTimestamp.getTime()
+}
+
+export const calculateUtilization = (intervalsByDay: Array<{
+ day: Date,
+ interval: IntervalsWithPeakFlag,
+ overlapsWith: Date[][]
+}>): Array<{ day: Date, utilization: number }> => {
+ const utilization: Array<{ day: Date, utilization: number }> = []
+
+ for (const intervals of intervalsByDay) {
+ const peakSecondsInDay = calculatePeakMilliseconds(intervals.interval)
+ let utilizationInSeconds = 0
+ for (const interval of intervals.overlapsWith) {
+ utilizationInSeconds += interval[1].getTime() - interval[0].getTime()
+ }
+ if (peakSecondsInDay === 0) {
+ utilization.push({ 'day': intervals.day, utilization: 0 })
+ } else {
+ utilization.push({
+ 'day': intervals.day,
+ utilization: (utilizationInSeconds * 100) / peakSecondsInDay
+ })
+ }
+ }
+
+ return utilization
+}
+
+export const arraysEqual = (arr1: any[], arr2: any[]): boolean => {
+ if (arr1.length !== arr2.length) {
+ return false;
+ }
+ for (let i = 0; i < arr1.length; i++) {
+ if (arr1[i] !== arr2[i]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+export const momentToTime = (date: any): string => {
+ const hours = (String(date.hour())).padStart(2, '0')
+ const minutes = (String(date.minute())).padStart(2, '0')
+ const seconds = (String(date.second())).padStart(2, '0')
+ return `${hours}:${minutes}:${seconds}`
+}
+
+export const addDays = (currentDate: Date, daysToAdd: number): Date => {
+ const dateCopy = new Date(currentDate.getTime());
+ dateCopy.setDate(dateCopy.getDate() + daysToAdd);
+ return dateCopy;
+}
+
+export const addHours = (currentDate: Date, hoursToAdd: number): Date => {
+ const dateCopy = new Date(currentDate.getTime());
+ dateCopy.setHours(dateCopy.getHours() + hoursToAdd);
+ return dateCopy;
+}
+
+export const addMilliseconds = (currentDate: Date, millisecondsToAdd: number): Date => {
+ const dateCopy = new Date(currentDate.getTime());
+ dateCopy.setMilliseconds(dateCopy.getMilliseconds() + millisecondsToAdd);
+ return dateCopy;
+}
+
+export const splitDailyIntervalsToHourly = (interval: IntervalsWithPeakFlag): Array> => {
+
+ const intervals: Array> = [];
+ let currentStart = new Date(interval.startTimestamp);
+ const end = new Date(interval.endTimestamp);
+
+ // Adjust the start time to the next full hour if it's not already a full hour
+ let firstIntervalEnd = new Date(currentStart);
+ firstIntervalEnd.setHours(firstIntervalEnd.getHours() + 1);
+ if (currentStart.getMinutes() !== 0) {
+ firstIntervalEnd.setMinutes(0, 0, 0); // Reset minutes, seconds, milliseconds
+ }
+ // First interval
+ intervals.push({
+ startTimestamp: currentStart,
+ endTimestamp: firstIntervalEnd,
+ isWorking: interval.isWorking
+ });
+ // Update currentStart to the end of the first interval
+ currentStart = firstIntervalEnd;
+ // Middle intervals
+ while (currentStart < end) {
+ let intervalEnd = new Date(currentStart);
+ intervalEnd.setHours(intervalEnd.getHours() + 1);
+ if (intervalEnd >= end) {
+ break; // Don't add this interval if it exceeds the endTime
+ }
+ intervals.push({
+ startTimestamp: currentStart,
+ endTimestamp: intervalEnd,
+ isWorking: interval.isWorking
+ });
+ currentStart = intervalEnd; // Prepare for the next iteration
+ }
+ // Final interval, if there's a remainder
+ if (currentStart < end) {
+ intervals.push({
+ startTimestamp: currentStart,
+ endTimestamp: end,
+ isWorking: interval.isWorking
+ });
+ }
+ return intervals;
+}
+
+export const patchZeroPoints = (
+ data: Array<{ day: Date, utilization: number }>,
+ from: Date,
+ to: Date,
+ frequency: TimeFrequency
+): Array<{ day: Date, utilization: number }> => {
+ if (data.length === 0) {
+ return [];
+ }
+ const patchedData: Array<{ day: Date, utilization: number }> = [];
+ if (frequency === TimeFrequency.DAILY) {
+ for (let i = 0; i < data.length - 1; i++) {
+ patchedData.push(data[i])
+ let currentDate = new Date(data[i].day);
+ let nextDate = new Date(data[i + 1].day);
+ while (getTimeFromEpoch(currentDate, frequency) + 1 < getTimeFromEpoch(nextDate, frequency)) {
+ currentDate = addDays(currentDate, 1)
+ patchedData.push({ day: currentDate, utilization: 0 })
+ }
+ }
+ // add last value
+ patchedData.push(data[data.length - 1])
+ } else {
+ for (let i = 0; i < data.length - 1; i++) {
+ patchedData.push(data[i])
+ let currentDate = data[i].day
+ let nextDate = new Date(data[i + 1].day);
+ while (getTimeFromEpoch(currentDate, frequency) + 1 < getTimeFromEpoch(nextDate, frequency)) {
+ currentDate = addHours(currentDate, 1)
+ patchedData.push({ day: currentDate, utilization: 0 })
+ }
+ }
+ // add last value
+ patchedData.push(data[data.length - 1])
+ }
+
+ return patchedData;
+}
+
+export const getTimeFromEpoch = (date: Date, frequency: TimeFrequency): number => {
+ const milliseconds = date.getTime();
+ switch (frequency) {
+ case TimeFrequency.HOURLY:
+ return Math.floor(milliseconds / 1000 / 3600);
+ case TimeFrequency.DAILY:
+ return Math.floor(milliseconds / 1000 / 3600 / 24);
+ default:
+ throw new Error('Invalid frequency provided');
+ }
+}
+
+export const divideTimeRangeToBusinessIntervals = (
+ rangeStart: Date,
+ rangeEnd: Date,
+ workingHours: {
+ startTime: string,
+ endTime: string
+ },
+ peakDays: Weekday[],
+ timeFrequency: TimeFrequency
+): Array> => {
+ const intervals: Array> = [];
+
+ let startTimeParts = workingHours.startTime.split(":").map(Number);
+ let endTimeParts = workingHours.endTime.split(":").map(Number);
+ let peakMillisecondsInDay = Math.abs(new Date().setHours(startTimeParts[0], startTimeParts[1], 0, 0) - new Date().setHours(endTimeParts[0], endTimeParts[1], 0, 0))
+ let nonPeakMillisecondsInDay = 24 * 60 * 60 * 1000 - peakMillisecondsInDay
+
+ const dayOfWeek = rangeStart.getDay();
+ const isWeekend = !peakDays.includes(dayOfWeek)
+
+ let currentDayPeakStart = new Date(new Date(rangeStart).setHours(startTimeParts[0], startTimeParts[1], 0, 0))
+ let currentDayNonPeakStart = new Date(new Date(rangeStart).setHours(endTimeParts[0], endTimeParts[1], 0, 0))
+ let nextDayPeakStart = addDays(new Date(new Date(rangeStart).setHours(startTimeParts[0], startTimeParts[1], 0, 0)), 1)
+ let nextDayNonPeakStart = addDays(new Date(new Date(rangeStart).setHours(endTimeParts[0], endTimeParts[1], 0, 0)), 1)
+
+ if (currentDayNonPeakStart > rangeEnd) {
+ currentDayNonPeakStart = rangeEnd
+ }
+ if (nextDayPeakStart > rangeEnd) {
+ nextDayPeakStart = rangeEnd
+ }
+ if (nextDayNonPeakStart > rangeEnd) {
+ nextDayNonPeakStart = rangeEnd
+ }
+
+ if (isWeekend) {
+ intervals.push({
+ startTimestamp: rangeStart,
+ endTimestamp: nextDayPeakStart,
+ isWorking: false
+ })
+ currentDayPeakStart = new Date(nextDayPeakStart)
+ } else {
+ if (currentDayPeakStart < currentDayNonPeakStart) {
+ if (rangeStart < currentDayPeakStart) {
+ intervals.push({
+ startTimestamp: rangeStart,
+ endTimestamp: currentDayPeakStart,
+ isWorking: false
+ })
+ // is same
+ // currentDayPeakStart = new Date(currentDayPeakStart)
+ } else if (currentDayPeakStart <= rangeStart && rangeStart < currentDayNonPeakStart) {
+ intervals.push({
+ startTimestamp: rangeStart,
+ endTimestamp: currentDayNonPeakStart,
+ isWorking: true
+ })
+ intervals.push({
+ startTimestamp: currentDayNonPeakStart,
+ endTimestamp: nextDayPeakStart,
+ isWorking: false
+ })
+ currentDayPeakStart = new Date(nextDayPeakStart)
+ } else {
+ intervals.push({
+ startTimestamp: rangeStart,
+ endTimestamp: nextDayPeakStart,
+ isWorking: false
+ })
+ currentDayPeakStart = new Date(nextDayPeakStart)
+ }
+ } else if (currentDayPeakStart > currentDayNonPeakStart) {
+ if (rangeStart < currentDayNonPeakStart) {
+ intervals.push({
+ startTimestamp: rangeStart,
+ endTimestamp: currentDayNonPeakStart,
+ isWorking: true
+ })
+ intervals.push({
+ startTimestamp: currentDayNonPeakStart,
+ endTimestamp: currentDayPeakStart,
+ isWorking: false
+ })
+ } else if (currentDayNonPeakStart <= rangeStart && rangeStart < currentDayPeakStart) {
+ intervals.push({
+ startTimestamp: rangeStart,
+ endTimestamp: currentDayPeakStart,
+ isWorking: false
+ })
+ // is same
+ // currentDayPeakStart = new Date(currentDayPeakStart)
+ } else {
+ intervals.push({
+ startTimestamp: rangeStart,
+ endTimestamp: nextDayNonPeakStart,
+ isWorking: true
+ })
+ intervals.push({
+ startTimestamp: nextDayNonPeakStart,
+ endTimestamp: nextDayPeakStart,
+ isWorking: false
+ })
+ currentDayPeakStart = new Date(nextDayPeakStart)
+ }
+ } else {
+ intervals.push({
+ startTimestamp: rangeStart,
+ endTimestamp: nextDayPeakStart,
+ isWorking: true
+ })
+ currentDayPeakStart = new Date(nextDayPeakStart)
+ }
+ }
+ let start = new Date(currentDayPeakStart)
+
+ while (rangeEnd >= start) {
+ const dayOfWeek = start.getDay();
+ const isWeekend = !peakDays.includes(dayOfWeek)
+
+ let currentDayPeakStart = new Date(new Date(start).setHours(startTimeParts[0], startTimeParts[1], 0, 0))
+ let currentDayNonPeakStart = addMilliseconds(currentDayPeakStart, peakMillisecondsInDay)
+ let nextDayPeakStart = addMilliseconds(currentDayNonPeakStart, nonPeakMillisecondsInDay)
+
+ if (isWeekend) {
+ intervals.push({
+ startTimestamp: start,
+ endTimestamp: nextDayPeakStart,
+ isWorking: false
+ })
+ } else {
+ if (currentDayPeakStart < currentDayNonPeakStart) {
+ if (currentDayNonPeakStart >= rangeEnd) {
+ intervals.push({
+ startTimestamp: currentDayPeakStart,
+ endTimestamp: rangeEnd,
+ isWorking: true
+ })
+ break
+ } else {
+ intervals.push({
+ startTimestamp: currentDayPeakStart,
+ endTimestamp: currentDayNonPeakStart,
+ isWorking: true
+ })
+ }
+ if (nextDayPeakStart >= rangeEnd) {
+ intervals.push({
+ startTimestamp: currentDayNonPeakStart,
+ endTimestamp: rangeEnd,
+ isWorking: false
+ })
+ break
+ } else {
+ intervals.push({
+ startTimestamp: currentDayNonPeakStart,
+ endTimestamp: nextDayPeakStart,
+ isWorking: false
+ })
+ }
+ } else {
+ intervals.push({
+ startTimestamp: currentDayPeakStart,
+ endTimestamp: nextDayPeakStart,
+ isWorking: true
+ })
+ currentDayPeakStart = new Date(nextDayPeakStart)
+ }
+ }
+ start = new Date(nextDayPeakStart)
+ }
+
+ if (timeFrequency === TimeFrequency.HOURLY) {
+ const hourlyIntervals: Array> = []
+ intervals.forEach((interval: IntervalsWithPeakFlag) => {
+ hourlyIntervals.push(...splitDailyIntervalsToHourly(interval))
+ })
+ return hourlyIntervals
+ }
+
+ return intervals
+}
diff --git a/src/datasources/asset/img/logo-ni.svg b/src/datasources/asset/img/logo-ni.svg
new file mode 100644
index 0000000..76f8758
--- /dev/null
+++ b/src/datasources/asset/img/logo-ni.svg
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/src/datasources/asset/module.ts b/src/datasources/asset/module.ts
new file mode 100644
index 0000000..9249cc1
--- /dev/null
+++ b/src/datasources/asset/module.ts
@@ -0,0 +1,11 @@
+import { DataSourcePlugin } from '@grafana/data';
+import { AssetDataSource } from './AssetDataSource';
+import { AssetQueryEditor } from './components/AssetQueryEditor';
+import { HttpConfigEditor } from 'core/components/HttpConfigEditor';
+import { AssetVariableQueryEditor } from "./components/AssetVariableQueryEditor";
+
+export const plugin = new DataSourcePlugin(AssetDataSource)
+ .setConfigEditor(HttpConfigEditor)
+ .setQueryEditor(AssetQueryEditor)
+ .setVariableQueryEditor(AssetVariableQueryEditor);
+
diff --git a/src/datasources/asset/plugin.json b/src/datasources/asset/plugin.json
new file mode 100644
index 0000000..71d8d8e
--- /dev/null
+++ b/src/datasources/asset/plugin.json
@@ -0,0 +1,15 @@
+{
+ "type": "datasource",
+ "name": "SystemLink Assets",
+ "id": "ni-slasset-datasource",
+ "metrics": true,
+ "info": {
+ "author": {
+ "name": "NI"
+ },
+ "logos": {
+ "small": "img/logo-ni.svg",
+ "large": "img/logo-ni.svg"
+ }
+ }
+}
diff --git a/src/datasources/asset/types.ts b/src/datasources/asset/types.ts
new file mode 100644
index 0000000..aed6ac0
--- /dev/null
+++ b/src/datasources/asset/types.ts
@@ -0,0 +1,323 @@
+import { DataQuery } from '@grafana/schema'
+import { DateTime, dateTime } from "@grafana/data";
+
+export interface AssetQuery extends DataQuery {
+ assetQueryType: AssetQueryType,
+ workspace: string,
+ entityType: EntityType
+ assetIdentifier: string,
+ isNIAsset: IsNIAsset,
+ minionId: string,
+ isPeak: IsPeak,
+ timeFrequency: TimeFrequency,
+ utilizationCategory: UtilizationCategory,
+ peakDays: Weekday[],
+ peakStart?: DateTime,
+ nonPeakStart?: DateTime,
+ policyOption: PolicyOption
+}
+
+export enum AssetQueryType {
+ METADATA = "METADATA",
+ UTILIZATION = "UTILIZATION"
+}
+
+export enum EntityType {
+ ASSET = "ASSET",
+ SYSTEM = "SYSTEM"
+}
+
+export enum TimeFrequency {
+ DAILY = 'DAILY',
+ HOURLY = 'HOURLY',
+}
+
+export enum IsNIAsset {
+ NIASSET = 'NIASSET',
+ NOTNIASSET = 'NOTNIASSET',
+ BOTH = 'BOTH'
+}
+
+export enum IsPeak {
+ PEAK = 'PEAK',
+ NONPEAK = 'NONPEAK',
+}
+
+export enum PolicyOption {
+ DEFAULT = "DEFAULT",
+ ALL = "ALL",
+ CUSTOM = "CUSTOM",
+}
+
+export enum Weekday {
+ Sunday = 0,
+ Monday = 1,
+ Tuesday = 2,
+ Wednesday = 3,
+ Thursday = 4,
+ Friday = 5,
+ Saturday = 6,
+}
+
+export enum UtilizationCategory {
+ TEST = 'TEST',
+ ALL = 'ALL',
+}
+
+export type ColumnDataType = 'BOOL' | 'INT32' | 'INT64' | 'FLOAT32' | 'FLOAT64' | 'STRING' | 'TIMESTAMP';
+
+export interface Column {
+ name: string;
+ dataType: ColumnDataType;
+ columnType: 'INDEX' | 'NULLABLE' | 'NORMAL';
+ properties: Record;
+}
+
+export type NumberTuple = [number, number]
+
+export interface AssetUtilizationHistory {
+ utilizationIdentifier: string,
+ assetIdentifier: string,
+ minionId: string,
+ category: string,
+ taskName?: string,
+ userName?: string,
+ startTimestamp: string,
+ endTimestamp?: string,
+ heartbeatTimestamp?: string,
+}
+
+export interface ServicePolicyModel {
+ calibrationPolicy: {
+ daysForApproachingCalibrationDueDate: number
+ },
+ workingHoursPolicy: {
+ startTime: string,
+ endTime: string
+ }
+}
+
+export interface Interval {
+ 'startTimestamp': T,
+ 'endTimestamp': T
+}
+
+export interface IntervalsWithPeakFlag extends Interval {
+ isWorking: boolean
+}
+
+export interface IntervalWithHeartbeat extends Interval {
+ 'heartbeatTimestamp': T
+}
+
+export interface WorkspaceResponse {
+ totalCount: number,
+ workspaces: Workspace[]
+}
+
+export interface Workspace {
+ "default": Boolean,
+ "enabled": Boolean,
+ "id": string,
+ "name": string
+}
+
+export interface AssetUtilizationHistoryResponse {
+ assetUtilizations: AssetUtilizationHistory[];
+ continuationToken: string;
+}
+
+export const defaultQuery = {
+ assetQueryType: AssetQueryType.METADATA,
+ workspace: '',
+ entityType: EntityType.ASSET,
+ assetIdentifier: '',
+ isNIAsset: IsNIAsset.BOTH,
+ minionId: '',
+ isPeak: IsPeak.PEAK,
+ timeFrequency: TimeFrequency.DAILY,
+ utilizationCategory: UtilizationCategory.ALL,
+ peakDays: [Weekday.Monday, Weekday.Tuesday, Weekday.Wednesday, Weekday.Thursday, Weekday.Friday],
+ peakStart: dateTime((new Date()).toISOString()),
+ nonPeakStart: dateTime((new Date(2024, 2, 24, 10, 20)).toISOString()),
+ policyOption: PolicyOption.DEFAULT
+};
+
+export interface SystemLinkError {
+ error: {
+ args: string[];
+ code: number;
+ message: string;
+ name: string;
+ }
+}
+
+export enum AssetUtilizationOrderBy {
+ UTILIZATION_IDENTIFIER = 'UTILIZATION_IDENTIFIER',
+ ASSET_IDENTIFIER = 'ASSET_IDENTIFIER',
+ MINION_ID = 'MINION_ID',
+ CATEGORY = 'CATEGORY',
+ TASK_NAME = 'TASK_NAME',
+ USER_NAME = 'USER_NAME',
+ START_TIMESTAMP = 'START_TIMESTAMP',
+}
+
+export interface TableMetadata {
+ columns: Column[];
+ id: string;
+ name: string;
+ workspace: string;
+}
+
+export interface AssetsResponse {
+ assets: AssetModel[],
+ totalCount: number
+}
+
+export interface AssetModel {
+ modelName: string,
+ modelNumber: number,
+ serialNumber: string,
+ vendorName: string,
+ vendorNumber: number,
+ busType: 'BUILT_IN_SYSTEM' | 'PCI_PXI' | 'USB' | 'GPIB' | 'VXI' | 'SERIAL' | 'TCP_IP' | 'CRIO' | 'SCXI' | 'CDAQ' | 'SWITCH_BLOCK' | 'SCC' | 'FIRE_WIRE' | 'ACCESSORY' | 'CAN' | 'SWITCH_BLOCK_DEVICE' | 'SLSC',
+ name: string,
+ assetType: 'GENERIC' | 'DEVICE_UNDER_TEST' | 'FIXTURE' | 'SYSTEM',
+ firmwareVersion: string,
+ hardwareVersion: string,
+ visaResourceName: string,
+ temperatureSensors: any[], // todo TemperatureSensorModel
+ supportsSelfCalibration: boolean,
+ supportsExternalCalibration: boolean,
+ customCalibrationInterval: number,
+ selfCalibration: any, // todo SelfCalibrationModel
+ isNIAsset: boolean,
+ workspace: string,
+ fileIds: string[],
+ supportsSelfTest: boolean,
+ supportsReset: boolean,
+ id: string,
+ location: AssetLocationModel,
+ calibrationStatus: 'OK' | 'APPROACHING_RECOMMENDED_DUE_DATE' | 'PAST_RECOMMENDED_DUE_DATE',
+ isSystemController: boolean,
+ externalCalibration?: ExternalCalibrationModel,
+ discoveryType: 'Manual' | 'Automatic',
+ properties: {
+ [key: string]: string
+ },
+ keywords: string[],
+ lastUpdatedTimestamp: string,
+}
+
+export interface AssetPresenceWithSystemConnectionModel {
+ assetPresence: AssetPresence,
+ systemConnection: SystemConnection
+}
+
+export interface AssetLocationModel {
+ minionId: string,
+ parent: string,
+ resourceUri: string,
+ slotNumber: number,
+ systemName: string,
+ state: AssetPresenceWithSystemConnectionModel
+}
+
+export interface ExternalCalibrationModel {
+ temperatureSensors: any[],
+ isLimited?: boolean,
+ date: string,
+ recommendedInterval: number,
+ nextRecommendedDate: string,
+ nextCustomDueDate?: string,
+ comments: string,
+ entryType: ExternalCalibrationEntryType,
+ operator: ExternalCalibrationOperatorModel
+}
+
+export interface ExternalCalibrationOperatorModel {
+ displayName: string;
+ userId: string
+}
+
+export interface UtilizationWithPercentageModel {
+ startTimestamp: string,
+ endTimestamp: string,
+ assetIdentifier: string,
+ assetName?: string,
+ minionId?: string,
+ category?: string,
+ percentage: string,
+}
+
+export enum ContainOperator {
+ CONTAIN,
+ NOTCONTAIN
+}
+
+export enum AssetFilterProperties {
+ AssetIdentifier = 'AssetIdentifier',
+ SerialNumber = 'SerialNumber',
+ ModelName = 'ModelName',
+ VendorName = 'VendorName',
+ VendorNumber = 'VendorNumber',
+ AssetName = 'AssetName',
+ FirmwareVersion = 'FirmwareVersion',
+ HardwareVersion = 'HardwareVersion',
+ BusType = 'BusType',
+ IsNIAsset = 'IsNIAsset',
+ Keywords = 'Keywords',
+ Properties = 'Properties',
+ LocationMinionId = 'Location.MinionId',
+ LocationSystemName = 'Location.SystemName',
+ LocationSlotNumber = 'Location.SlotNumber',
+ LocationAssetStateSystemConnection = 'Location.AssetState.SystemConnection',
+ LocationAssetStateAssetPresence = 'Location.AssetState.AssetPresence',
+ SupportsSelfCalibration = 'SupportsSelfCalibration',
+ SelfCalibrationCalibrationDate = 'SelfCalibration.CalibrationDate',
+ SupportsExternalCalibration = 'SupportsExternalCalibration',
+ CustomCalibrationInterval = 'CustomCalibrationInterval',
+ CalibrationStatus = 'CalibrationStatus',
+ ExternalCalibrationCalibrationDate = 'ExternalCalibration.CalibrationDate',
+ ExternalCalibrationNextRecommendedDate = 'ExternalCalibration.NextRecommendedDate',
+ ExternalCalibrationRecommendedInterval = 'ExternalCalibration.RecommendedInterval',
+ ExternalCalibrationComments = 'ExternalCalibration.Comments',
+ ExternalCalibrationIsLimited = 'ExternalCalibration.IsLimited',
+ ExternalCalibrationOperatorDisplayName = 'ExternalCalibration.Operator.DisplayName',
+ IsSystemController = 'IsSystemController'
+}
+
+export enum AssetPresence {
+ INITIALIZING = "INITIALIZING",
+ UNKNOWN = "UNKNOWN",
+ NOT_PRESENT = "NOT_PRESENT",
+ PRESENT = "PRESENT"
+}
+
+export enum ExternalCalibrationEntryType {
+ AUTOMATIC = 'AUTOMATIC',
+ MANUAL = 'MANUAL'
+}
+
+export enum SystemConnection {
+ APPROVED = "APPROVED",
+ DISCONNECTED = "DISCONNECTED",
+ CONNECTED_UPDATE_PENDING = "CONNECTED_UPDATE_PENDING",
+ CONNECTED_UPDATE_SUCCESSFUL = "CONNECTED_UPDATE_SUCCESSFUL",
+ CONNECTED_UPDATE_FAILED = "CONNECTED_UPDATE_FAILED",
+ UNSUPPORTED = "UNSUPPORTED",
+ ACTIVATED = "ACTIVATED"
+}
+
+export interface QueryAssetUtilizationHistoryRequest {
+ utilizationFilter?: string,
+ assetFilter?: string,
+ continuationToken?: string,
+ take?: number,
+ orderBy?: AssetUtilizationOrderBy,
+ orderByDescending?: boolean
+}
+
+export interface AssetVariableQuery {
+ minionId: string
+}
diff --git a/src/datasources/data-frame/components/DataFrameQueryEditor.tsx b/src/datasources/data-frame/components/DataFrameQueryEditor.tsx
index da98c28..55341b7 100644
--- a/src/datasources/data-frame/components/DataFrameQueryEditor.tsx
+++ b/src/datasources/data-frame/components/DataFrameQueryEditor.tsx
@@ -6,13 +6,13 @@ import { decimationMethods } from '../constants';
import _ from 'lodash';
import { getTemplateSrv } from '@grafana/runtime';
import { isValidId } from '../utils';
-import { FloatingError, parseErrorMessage } from '../errors';
+import { FloatingError, parseErrorMessage } from '../../../core/errors';
import { DataFrameQueryEditorCommon, Props } from './DataFrameQueryEditorCommon';
import { enumToOptions } from 'core/utils';
import { DataFrameQueryType } from '../types';
export const DataFrameQueryEditor = (props: Props) => {
- const [errorMsg, setErrorMsg] = useState('');
+ const [errorMsg, setErrorMsg] = useState('');
const handleError = (error: Error) => setErrorMsg(parseErrorMessage(error));
const common = new DataFrameQueryEditorCommon(props, handleError);
const tableMetadata = useAsync(() => common.datasource.getTableMetadata(common.query.tableId).catch(handleError), [common.query.tableId]);
diff --git a/src/datasources/data-frame/components/DataFrameVariableQueryEditor.tsx b/src/datasources/data-frame/components/DataFrameVariableQueryEditor.tsx
index 9cd60d4..150a8f5 100644
--- a/src/datasources/data-frame/components/DataFrameVariableQueryEditor.tsx
+++ b/src/datasources/data-frame/components/DataFrameVariableQueryEditor.tsx
@@ -3,12 +3,11 @@ import { AsyncSelect } from '@grafana/ui';
import { InlineField } from 'core/components/InlineField';
import { toOption } from '@grafana/data';
import { isValidId } from '../utils';
-import _ from 'lodash';
-import { FloatingError, parseErrorMessage } from '../errors';
+import { FloatingError, parseErrorMessage } from '../../../core/errors';
import { DataFrameQueryEditorCommon, Props } from './DataFrameQueryEditorCommon';
export function DataFrameVariableQueryEditor(props: Props) {
- const [errorMsg, setErrorMsg] = useState('');
+ const [errorMsg, setErrorMsg] = useState('');
const handleError = (error: Error) => setErrorMsg(parseErrorMessage(error));
const common = new DataFrameQueryEditorCommon(props, handleError);
@@ -31,4 +30,4 @@ export function DataFrameVariableQueryEditor(props: Props) {
);
-};
+}
diff --git a/src/datasources/data-frame/types.ts b/src/datasources/data-frame/types.ts
index 2047e61..1459386 100644
--- a/src/datasources/data-frame/types.ts
+++ b/src/datasources/data-frame/types.ts
@@ -64,16 +64,3 @@ export interface TableMetadataList {
export interface TableDataRows {
frame: { columns: string[]; data: string[][] };
}
-
-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);
-}
diff --git a/src/test/fixtures.ts b/src/test/fixtures.ts
index efb9dbb..11f2c18 100644
--- a/src/test/fixtures.ts
+++ b/src/test/fixtures.ts
@@ -2,6 +2,14 @@ import { DataQueryRequest, DataSourceApi, DataSourceInstanceSettings, LoadingSta
import { BackendSrv, BackendSrvRequest, FetchResponse, TemplateSrv } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { render } from '@testing-library/react';
+import {
+ Interval,
+ AssetModel,
+ IntervalsWithPeakFlag,
+ Weekday,
+ AssetUtilizationHistory,
+ ExternalCalibrationEntryType, AssetPresenceWithSystemConnectionModel
+} from 'datasources/asset/types';
import { Matcher, MatcherCreator, calledWithFn, mock } from 'jest-mock-extended';
import _ from 'lodash';
import React from 'react';
@@ -104,3 +112,112 @@ export function getQueryBuilder() {
});
};
}
+
+export const peakDaysMock: Weekday[] = [
+ Weekday.Monday,
+ Weekday.Tuesday,
+ Weekday.Wednesday,
+ Weekday.Thursday,
+ Weekday.Friday
+]
+
+export const startAMock = new Date('2024-03-12:00:00:00Z')
+export const endAMock = new Date('2024-03-12:02:00:00Z')
+export const startBMock = new Date('2024-03-12:01:00:00Z')
+export const endBMock = new Date('2024-03-12:03:00:00Z')
+export const intervalAMock: Array> = [
+ {
+ startTimestamp: startAMock,
+ endTimestamp: endAMock,
+ isWorking: true
+ }
+]
+export const intervalBMock: Array> = [
+ {
+ startTimestamp: startBMock.getTime(),
+ endTimestamp: endBMock.getTime()
+ }
+]
+
+export const AssetUtilizationHistoryMock: AssetUtilizationHistory[] = [
+ {
+ utilizationIdentifier: 'abc123',
+ assetIdentifier: '321cba',
+ minionId: '12345',
+ category: 'category A',
+ taskName: 'task A',
+ userName: 'User',
+ startTimestamp: '2023-11-20:00:00:00Z',
+ endTimestamp: '2023-11-21:00:00:00Z',
+ heartbeatTimestamp: '2023-11-22:00:00:00Z'
+ },
+ {
+ utilizationIdentifier: 'abc123',
+ assetIdentifier: '321cba',
+ minionId: '12345',
+ category: 'category A',
+ taskName: 'task A',
+ userName: 'User',
+ startTimestamp: '2023-11-20:00:00:00Z'
+ }
+]
+
+export const assetModelMock: AssetModel[] = [
+ {
+ assetType: "DEVICE_UNDER_TEST",
+ busType: "USB",
+ calibrationStatus: "APPROACHING_RECOMMENDED_DUE_DATE",
+ customCalibrationInterval: 123,
+ discoveryType: "Automatic",
+ externalCalibration: {
+ "temperatureSensors": [
+ {
+ "name": "Sensor0",
+ "reading": 25.7
+ }
+ ],
+ "isLimited": false,
+ "date": "2018-05-07T18:58:05.000Z",
+ "recommendedInterval": 12,
+ "nextRecommendedDate": "2019-05-07T18:58:05.000Z",
+ "nextCustomDueDate": "2019-06-07T18:58:05.000Z",
+ "comments": "This is a comment.",
+ "entryType": ExternalCalibrationEntryType.MANUAL,
+ "operator": {
+ "displayName": "John Doe",
+ "userId": "johnDoe2020"
+ }
+ },
+ fileIds: [''],
+ firmwareVersion: '123',
+ hardwareVersion: '',
+ id: '123',
+ modelName: '',
+ modelNumber: 123,
+ serialNumber: '',
+ vendorName: '',
+ vendorNumber: 123,
+ name: 'asset1',
+ visaResourceName: '',
+ temperatureSensors: [],
+ supportsSelfCalibration: true,
+ supportsExternalCalibration: true,
+ selfCalibration: {},
+ isNIAsset: true,
+ workspace: '',
+ supportsSelfTest: true,
+ supportsReset: true,
+ location: {
+ minionId: 'minion1',
+ parent: '',
+ resourceUri: '',
+ slotNumber: 123,
+ systemName: 'system1',
+ state: {} as AssetPresenceWithSystemConnectionModel
+ },
+ lastUpdatedTimestamp: '',
+ isSystemController: true,
+ keywords: [],
+ properties: {}
+ }
+]