From 85b70849882ffac7198d5ba08794c08760b96bc6 Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 10:31:50 +0200 Subject: [PATCH 01/21] feat(common-backend-insight-hub): iot time series Add functionality to check if session is runned locally Co-authored-by: Jonas --- .../services/iot-time-series.service.spec.ts | 55 +++++++++++++++++++ .../lib/services/iot-time-series.service.ts | 17 +++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.spec.ts b/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.spec.ts index 6981984b..bbbbfb5f 100644 --- a/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.spec.ts +++ b/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.spec.ts @@ -9,6 +9,7 @@ import { ITimeSeriesRequestParameter } from '../models'; import { INSIGHT_HUB_OPTIONS } from '../tokens'; import { XdIotTimeSeriesService } from './iot-time-series.service'; import { XdTokenManagerService } from './token-manager.service'; +import { IInsightHub } from 'common-backend-models'; interface MockSelectParameter { flow: number; @@ -18,6 +19,7 @@ interface MockSelectParameter { describe('XdIotTimeSeriesService', () => { let service: XdIotTimeSeriesService; let httpService: HttpService; + let insightHubOptions: IInsightHub; beforeEach(async () => { const httpServiceMock = { @@ -55,6 +57,7 @@ describe('XdIotTimeSeriesService', () => { service = module.get(XdIotTimeSeriesService); httpService = module.get(HttpService); + insightHubOptions = module.get(INSIGHT_HUB_OPTIONS); }); it('should be defined', () => { @@ -99,4 +102,56 @@ describe('XdIotTimeSeriesService', () => { expect(response).toEqual(mockResponse); }); }); + + describe('isLocalSession', () => { + it('should return true for isLocalSession when apiKey and apiUrl are undefined', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + insightHubOptions.apiKey = null; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + insightHubOptions.apiUrl = null; + + expect(service.isLocalSession()).toBe(true); + }); + + it('should return true for isLocalSession when apiKey and apiUrl are empty strings', () => { + insightHubOptions.apiKey = ''; + insightHubOptions.apiUrl = ''; + + expect(service.isLocalSession()).toBe(true); + }); + + it('should return true for isLocalSession when apiKey is defined and apiUrl is undefined', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + insightHubOptions.apiUrl = null; + + expect(service.isLocalSession()).toBe(true); + }); + + it('should return true for isLocalSession when apiKey is undefined and apiUrl is defined', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + insightHubOptions.apiKey = null; + + expect(service.isLocalSession()).toBe(true); + }); + + it('should return true for isLocalSession when apiKey is defined and apiUrl is an empty string', () => { + insightHubOptions.apiUrl = ''; + + expect(service.isLocalSession()).toBe(true); + }); + + it('should return true for isLocalSession when apiKey is an empty string and apiUrl is defined', () => { + insightHubOptions.apiKey = ''; + + expect(service.isLocalSession()).toBe(true); + }); + + it('should return false for isLocalSession when apiKey and apiUrl are defined', () => { + expect(service.isLocalSession()).toBe(false); + }); + }); }); diff --git a/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.ts b/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.ts index e0164526..b60a034c 100644 --- a/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.ts +++ b/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.ts @@ -1,5 +1,5 @@ import { HttpService } from '@nestjs/axios'; -import { Inject, Injectable, Logger } from '@nestjs/common'; +import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { IInsightHub } from 'common-backend-models'; import { Observable } from 'rxjs'; @@ -12,7 +12,7 @@ import { XdTokenManagerService } from './token-manager.service'; * Service to interact with the IoT Time Series API. */ @Injectable() -export class XdIotTimeSeriesService extends XdBaseBearerInteractionService { +export class XdIotTimeSeriesService extends XdBaseBearerInteractionService implements OnModuleInit { constructor( private readonly httpClient: HttpService, @Inject(INSIGHT_HUB_OPTIONS) @@ -29,7 +29,11 @@ export class XdIotTimeSeriesService extends XdBaseBearerInteractionService { ); } - /** + onModuleInit(): any { + console.log('localSession ', this.isLocalSession()) + } + + /** * Allows to get the time series data from the IoT Time Series API. * @see https://documentation.mindsphere.io/MindSphere/apis/iot-iottimeseries/api-iottimeseries-api.html * @@ -44,4 +48,11 @@ export class XdIotTimeSeriesService extends XdBaseBearerInteractionService { ): Observable { return super._getData(`${assetId}/${propertySetName}`, params); } + + /** + * Checks if the session is local or not. + */ + public isLocalSession(): boolean { + return !this.insightHubOptions.apiKey || !this.insightHubOptions.apiUrl; + } } From 0589e532daaf8bd1498750767c5c399e23301bbe Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 10:33:13 +0200 Subject: [PATCH 02/21] docs: env Add env explanations Co-authored-by: Jonas --- .env.example | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.env.example b/.env.example index 335fb2b2..8ffb790b 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,11 @@ POSTGRES_DB=amos # Full database connection URL constructed from the above PostgreSQL variables used for Prisma DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} +# The Api Url for the Insight Hub API +INSIGHT_HUB_API_URL=https://gateway.eu1.mindsphere.io/api +# The Api Key for the Insight Hub API +INSIGHT_HUB_API_KEY=Y2FzdGlkZXYtYW1vcy12MS4wLjA6aG05N0NMMGRaZGU1YTl6d2Q3ckRlOVpuRUhwR3plWndFT21vQ2s5MTIzSA== + # Frontend XD_API_URL=http://${BACKEND_HOST}:${BACKEND_PORT} SECRET_KEY=SecretKey From 36330653301db9141443cc21fbd9e45ea0851d2e Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 10:33:48 +0200 Subject: [PATCH 03/21] revert(common-backend-insight-hub): onModuleInit Add functionality to check if session is runned locally Co-authored-by: Jonas --- .../insight-hub/src/lib/services/iot-time-series.service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.ts b/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.ts index b60a034c..18fc8145 100644 --- a/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.ts +++ b/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.ts @@ -29,10 +29,6 @@ export class XdIotTimeSeriesService extends XdBaseBearerInteractionService imple ); } - onModuleInit(): any { - console.log('localSession ', this.isLocalSession()) - } - /** * Allows to get the time series data from the IoT Time Series API. * @see https://documentation.mindsphere.io/MindSphere/apis/iot-iottimeseries/api-iottimeseries-api.html From 52b5bd3da0cba7efe79df2500e6aef12311406fd Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 10:33:59 +0200 Subject: [PATCH 04/21] revert(common-backend-insight-hub): onModuleInit Add functionality to check if session is runned locally Co-authored-by: Jonas --- .../insight-hub/src/lib/services/iot-time-series.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.ts b/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.ts index 18fc8145..713b72f6 100644 --- a/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.ts +++ b/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.ts @@ -12,7 +12,7 @@ import { XdTokenManagerService } from './token-manager.service'; * Service to interact with the IoT Time Series API. */ @Injectable() -export class XdIotTimeSeriesService extends XdBaseBearerInteractionService implements OnModuleInit { +export class XdIotTimeSeriesService extends XdBaseBearerInteractionService { constructor( private readonly httpClient: HttpService, @Inject(INSIGHT_HUB_OPTIONS) From d090b94acd7953f78e9539f9be1289c0c510da09 Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 10:34:06 +0200 Subject: [PATCH 05/21] revert(common-backend-insight-hub): onModuleInit Add functionality to check if session is runned locally Co-authored-by: Jonas --- .../insight-hub/src/lib/services/iot-time-series.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.ts b/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.ts index 713b72f6..c1005426 100644 --- a/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.ts +++ b/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.ts @@ -1,5 +1,5 @@ import { HttpService } from '@nestjs/axios'; -import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { Inject, Injectable, Logger } from '@nestjs/common'; import { IInsightHub } from 'common-backend-models'; import { Observable } from 'rxjs'; From 4e20135798be248079f33db56cb37cebf3ed423a Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 12:43:41 +0200 Subject: [PATCH 06/21] refactor(backend): insight hub key optional Co-authored-by: Jonas --- apps/backend/src/app/config/classes/environment.class.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/app/config/classes/environment.class.ts b/apps/backend/src/app/config/classes/environment.class.ts index 387761b4..c274f638 100644 --- a/apps/backend/src/app/config/classes/environment.class.ts +++ b/apps/backend/src/app/config/classes/environment.class.ts @@ -82,7 +82,7 @@ export class EnvironmentVariables implements IEnvironmentVariables { /** * The URL of the API to use for the IotTimeSeriesService */ - @IsDefined() + @IsOptional() @IsString() @MinLength(1) INSIGHT_HUB_API_URL?: string; @@ -90,7 +90,7 @@ export class EnvironmentVariables implements IEnvironmentVariables { /** * The API key to use for the IotTimeSeriesService */ - @IsDefined() + @IsOptional() @IsString() @MinLength(1) INSIGHT_HUB_API_KEY?: string; From bfc2b0fcc2436c646dadc5655ccd6fb0a54ab543 Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 12:45:48 +0200 Subject: [PATCH 07/21] refactor(facilities-backend-timeseries): services, controller Add timeseries and offline online functionality Co-authored-by: Jonas --- .../domain/src/lib/application/facades/details.facade.ts | 4 ++-- .../frontend/view/src/lib/components/detail/detail.page.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/facilities/frontend/domain/src/lib/application/facades/details.facade.ts b/libs/facilities/frontend/domain/src/lib/application/facades/details.facade.ts index c73ae0d4..6927286f 100644 --- a/libs/facilities/frontend/domain/src/lib/application/facades/details.facade.ts +++ b/libs/facilities/frontend/domain/src/lib/application/facades/details.facade.ts @@ -1,6 +1,6 @@ import { inject, Injectable } from '@angular/core'; import { faker } from '@faker-js/faker'; -import { map } from 'rxjs'; +import { lastValueFrom, map, tap } from 'rxjs'; import { FacilitiesRequestService } from '../../infrastructure/facilities-request.service'; import { TimeSeriesRequestService } from '../../infrastructure/timeseries-request.service'; @@ -57,7 +57,7 @@ export class XdDetailsFacade { * @param queryParams The query parameters. */ public getTimeSeriesDataItems(assetId: string, propertySetName: string, queryParams: any) { - return this._timeseriesService.getTimeSeriesDataItems( + return this._timeseriesService.getTimeSeriesDataItems( { assetId, propertySetName }, queryParams, ); diff --git a/libs/facilities/frontend/view/src/lib/components/detail/detail.page.ts b/libs/facilities/frontend/view/src/lib/components/detail/detail.page.ts index 0676965c..1a8ecbd8 100644 --- a/libs/facilities/frontend/view/src/lib/components/detail/detail.page.ts +++ b/libs/facilities/frontend/view/src/lib/components/detail/detail.page.ts @@ -74,6 +74,7 @@ export class XdDetailPage implements OnInit { to: this._currentTime, }), ); + private readonly defaultOptions: EChartsOption = { tooltip: { trigger: 'axis', From c960477223bc36731773b4bd3e3c2ca200e13ba4 Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 12:48:43 +0200 Subject: [PATCH 08/21] refactor(facilities-backend-timeseries): services, controller Add timeseries and offline online functionality Co-authored-by: Jonas --- .../lib/controller/timeseries.controller.ts | 13 +- .../lib/services/timeseries.service.spec.ts | 62 +++- .../src/lib/services/timeseries.service.ts | 278 +++++++++--------- 3 files changed, 201 insertions(+), 152 deletions(-) diff --git a/libs/facilities/backend/timeseries/src/lib/controller/timeseries.controller.ts b/libs/facilities/backend/timeseries/src/lib/controller/timeseries.controller.ts index 422013ea..12abcc69 100644 --- a/libs/facilities/backend/timeseries/src/lib/controller/timeseries.controller.ts +++ b/libs/facilities/backend/timeseries/src/lib/controller/timeseries.controller.ts @@ -47,14 +47,9 @@ export class XdTimeseriesController { @Param() params: GetTimeSeriesParamsDto, @Query() query: GetTimeSeriesQueryDto, ): Observable { - const { local = false, ...rest } = query; - const args = { - ...params, - ...rest, - }; - - return local - ? this.timeseriesService.getTimeSeriesFromDB(args) - : this.timeseriesService.getTimeSeriesFromApi(args); + return this.timeseriesService.getTimeSeries({ + ...params, + ...query, + }); } } diff --git a/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.spec.ts b/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.spec.ts index 9bf23c89..abd808ab 100644 --- a/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.spec.ts +++ b/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.spec.ts @@ -63,6 +63,7 @@ describe('TimeseriesService', () => { provide: XdIotTimeSeriesService, useValue: { getTimeSeriesData: jest.fn().mockReturnValue(of([])), + isLocalSession: jest.fn().mockReturnValue(true), }, }, { @@ -195,11 +196,6 @@ describe('TimeseriesService', () => { .spyOn(prisma.timeSeriesDataItem, 'findMany') .mockResolvedValue([]); - jest.spyOn(prisma.timeSeriesItem, 'findUnique').mockResolvedValue({ - assetId: faker.string.uuid(), - propertySetName: faker.string.sample(), - variables: {}, - }); const params: IGetTimeSeriesParams = { assetId: faker.string.uuid(), @@ -217,14 +213,13 @@ describe('TimeseriesService', () => { }), ); - expect(getTimeSeriesDataSpy).toHaveBeenCalledTimes(1); expect(findManySpy).toHaveBeenCalledTimes(0); expect(getTimeSeriesDataSpy).toHaveBeenCalledWith( params.assetId, params.propertySetName, - omit(query, 'select'), + query ); await lastValueFrom( @@ -238,6 +233,59 @@ describe('TimeseriesService', () => { expect(findManySpy).toHaveBeenCalledTimes(1); }); + it('should use local db when api iot service decides its a local session', async ()=> { + const getTimeSeriesDataSpy = jest + .spyOn(iothub, 'getTimeSeriesData') + .mockReturnValue(of([])); + + const findManySpy = jest + .spyOn(prisma.timeSeriesDataItem, 'findMany') + .mockResolvedValue([]); + + const isLocalSessionSpy = jest + .spyOn(iothub, 'isLocalSession') + + + const params: IGetTimeSeriesParams = { + assetId: faker.string.uuid(), + propertySetName: faker.string.sample(), + }; + + const query: IGetTimeseriesQuery = { + select: [ 'flow' ], + }; + + isLocalSessionSpy.mockReturnValue(false) + + await lastValueFrom( + service.getTimeSeries({ + ...params, + ...query, + }), + ); + + + expect(findManySpy).toHaveBeenCalledTimes(0); + + expect(getTimeSeriesDataSpy).toHaveBeenCalledWith( + params.assetId, + params.propertySetName, + query + ); + + isLocalSessionSpy.mockReturnValue(true) + + await lastValueFrom( + service.getTimeSeries({ + ...params, + ...query, + }), + ); + + expect(getTimeSeriesDataSpy).toHaveBeenCalledTimes(1); + expect(findManySpy).toHaveBeenCalledTimes(1); + }) + it('should use the correct args to query the time series data', async () => { const findManySpy = jest .spyOn(prisma.timeSeriesDataItem, 'findMany') diff --git a/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.ts b/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.ts index b656376c..33c3439b 100644 --- a/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.ts +++ b/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.ts @@ -6,10 +6,13 @@ import { IGetTimeSeriesParams, IGetTimeseriesQuery, ITimeSeriesDataItemResponse, - ITimeSeriesItemResponse, ITimeSeriesPumpReport, + ITimeSeriesItemResponse } from 'facilities-shared-models'; -import { pick } from 'lodash'; import { catchError, from, map, Observable, switchMap } from 'rxjs'; +import { omit } from 'lodash'; +import dayjs = require('dayjs'); +import { Dayjs } from 'dayjs'; + @Injectable() export class XdTimeseriesService { @@ -20,155 +23,158 @@ export class XdTimeseriesService { private readonly iotTimeSeriesService: XdIotTimeSeriesService, ) {} + /** + * Acts as a gateway to get the time series data from either the API or the DB. + * + * @param args - the query and parameters based arguments to get the time series data + */ + public getTimeSeries(args: IGetTimeSeriesParams & IGetTimeseriesQuery){ + return !this.iotTimeSeriesService.isLocalSession()? + this.getTimeSeriesFromDB(args): + this.getTimeSeriesFromApi(args); + } + /** * Get timeseries data based on the assetId and propertySetName from the API + * + * @param args - the query and parameters based arguments to get the time series data */ public getTimeSeriesFromApi( args: IGetTimeSeriesParams & IGetTimeseriesQuery, ): Observable { - const { assetId, propertySetName, sort, select, ...params } = args; + const { assetId, propertySetName, sort, ...params } = args; - return from( - this.prismaService.timeSeriesItem.findUnique({ - where: { assetId_propertySetName: { assetId, propertySetName } }, - }), - ).pipe( - map((item) => { - if (!item) { - throw new HttpException( - `No timeseries data found for assetId: ${assetId} and propertySetName: ${propertySetName}`, - HttpStatus.NOT_FOUND, - ); - } - return item; - }), - switchMap(() => { - return this.iotTimeSeriesService - .getTimeSeriesData< - any, - { - _time: string; - - [key: string]: any; - }[] - >(assetId, propertySetName, { - ...params, - // Todo: Fix this in a future PR - sort: sort as unknown as ETimeSeriesOrdering, - }) - .pipe( - map((items) => { - const data = items.map((item) => { - const { _time, ...rest } = item; - return { - ...rest, - time: new Date(_time), - }; - }); - const { status, indicatorMsg, metrics } = checkPumpStatus( - data as unknown as ITimeSeriesPumpReport[], - ); - - const timeSeriesData = data.map(({ time, ...rest }) => { - return this.prismaService.timeSeriesDataItem.upsert({ - where: { - timeSeriesItemAssetId_timeSeriesItemPropertySetName_time: { - timeSeriesItemAssetId: assetId, - timeSeriesItemPropertySetName: propertySetName, - time: time, - }, - }, - update: {}, - create: { - time: time, - timeSeriesItemAssetId: assetId, - timeSeriesItemPropertySetName: propertySetName, - data: rest, - }, - }); - }); - - const updatedPumpData = this.prismaService.asset.upsert({ - where: { - assetId, - }, - update: { - status, - indicatorMsg, - metrics: { - deleteMany: {}, - create: metrics - } - }, - create: { - assetId, - status, - indicatorMsg, - location: { - create: { - latitude: 0, - longitude: 0, - }, - }, - name: 'Pump', - typeId: 'pump', - metrics: { - create: metrics - } - }, - }); - - this.prismaService.$transaction([ ...timeSeriesData, updatedPumpData ]); - - if (select) { - return data.map((item) => ({ - ...pick(item, select), - time: item.time, - })); - } - - return data; - }), - ); - }), - ); - } + return this.iotTimeSeriesService + .getTimeSeriesData< + any, + { + _time: string; + [key: string]: any; + }[] + >(assetId, propertySetName, { + ...params, + // Todo: Fix this in a future PR + sort: sort as unknown as ETimeSeriesOrdering, + }).pipe( + map((items) => { + return items.map( + (item) => ({ + time: new Date(item._time), + ...omit(item, "_time") + }) + ) + } + ) + ) + } + + private findFirstTime(assetId: string, propertySetName: string) { + return from( + this.prismaService.timeSeriesDataItem.findFirst({ + where: { + timeSeriesItemAssetId: assetId, + timeSeriesItemPropertySetName: propertySetName, + }, + orderBy: { + time: 'desc', + }, + })); + } + + private normalizeTimes(time: Date, from?: Date, to?: Date) { + let normalizedFromTime: Date | undefined; + let normalizedToTime: Date | undefined; + const timeDifference = dayjs().diff(time, 'millisecond', true); + + if (from) { + normalizedFromTime = dayjs(from).subtract(timeDifference, 'millisecond').toDate(); + } + if (to) { + normalizedToTime = dayjs(to).subtract(timeDifference, 'millisecond').toDate(); + } + + return { normalizedFromTime, normalizedToTime, timeDifference }; + } /** * Get timeseries data based on the assetId and propertySetName from the DB + * + * @param args - the query and parameters based arguments to get the time series data */ public getTimeSeriesFromDB( args: IGetTimeSeriesParams & IGetTimeseriesQuery, ): Observable { - const { assetId, propertySetName } = args; + const { assetId, propertySetName } = args - return from( - this.prismaService.timeSeriesDataItem.findMany({ - where: { - timeSeriesItemAssetId: assetId, - timeSeriesItemPropertySetName: propertySetName, - time: { - gte: args.from, - lte: args.to, - }, - }, - take: args.limit, - orderBy: { - time: args.sort, - }, - }), - ).pipe( - map((items) => { - return items.map((item) => ({ - time: item.time, - ...this.prismaService.selectKeysFromJSON(item.data, args.select), - })); - }), - catchError((err: Error) => { - throw err; - }), - ); - } + + return this.findFirstTime(assetId, propertySetName).pipe( + switchMap((item) => { + if (!item) { + throw new HttpException('timeSeriesItem not found', HttpStatus.NOT_FOUND); + } + + const { normalizedFromTime, normalizedToTime, timeDifference } = this.normalizeTimes(item.time, args.from, args.to); + + return from( + this.prismaService.timeSeriesDataItem.findMany({ + where: { + timeSeriesItemAssetId: assetId, + timeSeriesItemPropertySetName: propertySetName, + time: { + gte: normalizedFromTime, + lte: normalizedToTime, + }, + }, + take: args.limit, + orderBy: { + time: args.sort, + }, + }) + ).pipe( + map((result) => ({ result, timeDifference })) + ); + }), + map(({ result, timeDifference }) => { + if (!Array.isArray(result)) { + throw new HttpException('Unexpected result format', HttpStatus.INTERNAL_SERVER_ERROR); + } + + return result.map((item) => ({ + time: dayjs(item.time).add(timeDifference, 'millisecond').toDate(), + ...this.prismaService.selectKeysFromJSON(item.data, args.select), + })); + }), + ); + } + + + // return from( + // this.prismaService.timeSeriesDataItem.findMany({ + // where: { + // timeSeriesItemAssetId: assetId, + // timeSeriesItemPropertySetName: propertySetName, + // time: { + // gte: args.from, + // lte: args.to, + // }, + // }, + // take: args.limit, + // orderBy: { + // time: args.sort, + // }, + // }), + // ).pipe( + // map((items) => { + // return items.map((item) => ({ + // time: item.time, + // ...this.prismaService.selectKeysFromJSON(item.data, args.select), + // })); + // }), + // catchError((err: Error) => { + // throw err; + // }), + // ); + // } /** * Get all timeseries data From b3aa819a6fb5caafe3c33c9eac1b7c2212ff91cc Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 12:49:37 +0200 Subject: [PATCH 09/21] refactor: prisma adjust seed script Co-authored-by: Jonas --- prisma/seed.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 85c27369..8c80199a 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -97,11 +97,11 @@ async function seedSingleFacility({ timeSeriesItemPropertySetName: tsItemPumpData.propertySetName, data: { - motorCurrent: data.MotorCurrent, - pressureOut: data.PressureOut, - stuffingBoxTemperature: data.StuffingBoxTemperature, - pressureIn: data.PressureIn, - flow: data.Flow, + MotorCurrent: data.MotorCurrent, + PressureOut: data.PressureOut, + StuffingBoxTemperature: data.StuffingBoxTemperature, + PressureIn: data.PressureIn, + Flow: data.Flow, } as Prisma.JsonObject, }; }); @@ -135,12 +135,12 @@ async function seedSingleFacility({ timeSeriesItemPropertySetName: tSItemEnv.propertySetName, data: { - pressureQc: data.Pressure_qc, - temperature: data.Temperature, - temperatureQc: data.Temperature_qc, - humidityQc: data.Humidity_qc, - humidity: data.Humidity, - pressure: data.Pressure, + PressureQc: data.Pressure_qc, + Temperature: data.Temperature, + TemperatureQc: data.Temperature_qc, + HumidityQc: data.Humidity_qc, + Humidity: data.Humidity, + Pressure: data.Pressure, } as Prisma.JsonObject, }; }); From 5986c0d786bed81b4051799666df565cd7fbb91f Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 16:20:37 +0200 Subject: [PATCH 10/21] feat(common-backend-swagger): metrics route Co-authored-by: Jonas --- .../swagger/src/lib/const/swagger-tag-information.const.ts | 4 ++++ libs/common/backend/swagger/src/lib/enums/swagger-tag.enum.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/libs/common/backend/swagger/src/lib/const/swagger-tag-information.const.ts b/libs/common/backend/swagger/src/lib/const/swagger-tag-information.const.ts index 5d365919..2e09acc4 100644 --- a/libs/common/backend/swagger/src/lib/const/swagger-tag-information.const.ts +++ b/libs/common/backend/swagger/src/lib/const/swagger-tag-information.const.ts @@ -17,4 +17,8 @@ export const SWAGGER_TAG_INFORMATION: Record Date: Sat, 13 Jul 2024 16:21:30 +0200 Subject: [PATCH 11/21] feat(backend): metrics module Co-authored-by: Jonas --- apps/backend/src/app/app.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/backend/src/app/app.module.ts b/apps/backend/src/app/app.module.ts index afe8beb5..3e9a23b4 100644 --- a/apps/backend/src/app/app.module.ts +++ b/apps/backend/src/app/app.module.ts @@ -1,3 +1,4 @@ +import { XdMetricsModule } from '@frontend/facilities/backend/metrics'; import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { XdCaseManagementModule } from 'cases-backend-case-management'; @@ -24,6 +25,7 @@ import { validateConfig } from './config/validation'; XdTimeseriesModule, XdCaseManagementModule, XdFacilitiesBackendFacilitiesModule, + XdMetricsModule ], controllers: [], providers: [], From 63cafe44e4e91b2459860daf939241456861b14d Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 16:24:50 +0200 Subject: [PATCH 12/21] style: primsa --- prisma/schema.prisma | 46 ++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d96b9ea1..fefa8a92 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -37,18 +37,18 @@ model TimeSeriesDataItem { } model Asset { - assetId String @id - name String - description String? - location AssetLocation? - typeId String - variables Json? - status FacilityStatus @default(REGULAR) - indicatorMsg String @default("The pump is working as expected.") + assetId String @id + name String + description String? + location AssetLocation? + typeId String + variables Json? + status FacilityStatus @default(REGULAR) + indicatorMsg String @default("The pump is working as expected.") cases Case[] timeSeriesItems TimeSeriesItem[] - metrics Metrics[] + metrics Metrics[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -97,20 +97,20 @@ model Case { } model Metrics { - id Int @id @default(autoincrement()) - min Float? - max Float? - mean Float? - variance Float? - standardDeviation Float? - coefficientOfVariation Float? - name String - - assetId String - Asset Asset @relation(fields: [assetId], references: [assetId]) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id Int @id @default(autoincrement()) + min Float? + max Float? + mean Float? + variance Float? + standardDeviation Float? + coefficientOfVariation Float? + name String + + assetId String + Asset Asset @relation(fields: [assetId], references: [assetId]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } enum FacilityStatus { From 9b49844b266ec00eb34c4ca72bed3aeb9182bbb6 Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 16:27:48 +0200 Subject: [PATCH 13/21] docs: add key description Co-authored-by: Jonas --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 8ffb790b..d13889bc 100644 --- a/.env.example +++ b/.env.example @@ -21,7 +21,7 @@ DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:$ # The Api Url for the Insight Hub API INSIGHT_HUB_API_URL=https://gateway.eu1.mindsphere.io/api # The Api Key for the Insight Hub API -INSIGHT_HUB_API_KEY=Y2FzdGlkZXYtYW1vcy12MS4wLjA6aG05N0NMMGRaZGU1YTl6d2Q3ckRlOVpuRUhwR3plWndFT21vQ2s5MTIzSA== +INSIGHT_HUB_API_KEY=KEY # Frontend XD_API_URL=http://${BACKEND_HOST}:${BACKEND_PORT} From 7c856eeed99882c028e2cf3e1255d3488a9ea385 Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 16:28:18 +0200 Subject: [PATCH 14/21] build: metrics lib Co-authored-by: Jonas --- tsconfig.base.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tsconfig.base.json b/tsconfig.base.json index a05240cf..21dc565b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -18,6 +18,9 @@ "paths": { "@frontend/cases/frontend/domain": ["libs/cases/frontend/domain/src/index.ts"], "@frontend/common/shared/models": ["libs/common/shared/models/src/index.ts"], + "@frontend/facilities/backend/metrics": [ + "libs/facilities/backend/metrics/src/index.ts" + ], "@frontend/facilities/backend/models": ["libs/facilities/backend/models/src/index.ts"], "@frontend/facilities/backend/utils": ["libs/facilities/backend/utils/src/index.ts"], "@frontend/facilities/frontend/domain": [ From 6f49ab6d150946f07635d6b466bbcc446dae1bfc Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 16:29:45 +0200 Subject: [PATCH 15/21] feat(common-backend-insight-hub): integrate local functionality Co-authored-by: Jonas --- .../base-bearer-interaction.service.spec.ts | 2 +- .../services/iot-time-series.service.spec.ts | 86 +++++++++---------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/libs/common/backend/insight-hub/src/lib/services/base-bearer-interaction.service.spec.ts b/libs/common/backend/insight-hub/src/lib/services/base-bearer-interaction.service.spec.ts index 0da0d677..b93f5861 100644 --- a/libs/common/backend/insight-hub/src/lib/services/base-bearer-interaction.service.spec.ts +++ b/libs/common/backend/insight-hub/src/lib/services/base-bearer-interaction.service.spec.ts @@ -1,6 +1,6 @@ import { faker } from '@faker-js/faker'; import { HttpService } from '@nestjs/axios'; -import { HttpException, HttpStatus, Inject, Injectable, Logger } from '@nestjs/common'; +import { HttpException, Inject, Injectable, Logger } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { AxiosResponse } from 'axios'; import { IInsightHub } from 'common-backend-models'; diff --git a/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.spec.ts b/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.spec.ts index bbbbfb5f..52ea6c66 100644 --- a/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.spec.ts +++ b/libs/common/backend/insight-hub/src/lib/services/iot-time-series.service.spec.ts @@ -3,13 +3,13 @@ import { HttpService } from '@nestjs/axios'; import { Logger } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { AxiosResponse } from 'axios'; +import { IInsightHub } from 'common-backend-models'; import { firstValueFrom, Observable, of } from 'rxjs'; import { ITimeSeriesRequestParameter } from '../models'; import { INSIGHT_HUB_OPTIONS } from '../tokens'; import { XdIotTimeSeriesService } from './iot-time-series.service'; import { XdTokenManagerService } from './token-manager.service'; -import { IInsightHub } from 'common-backend-models'; interface MockSelectParameter { flow: number; @@ -19,7 +19,7 @@ interface MockSelectParameter { describe('XdIotTimeSeriesService', () => { let service: XdIotTimeSeriesService; let httpService: HttpService; - let insightHubOptions: IInsightHub; + let insightHubOptions: IInsightHub; beforeEach(async () => { const httpServiceMock = { @@ -57,7 +57,7 @@ describe('XdIotTimeSeriesService', () => { service = module.get(XdIotTimeSeriesService); httpService = module.get(HttpService); - insightHubOptions = module.get(INSIGHT_HUB_OPTIONS); + insightHubOptions = module.get(INSIGHT_HUB_OPTIONS); }); it('should be defined', () => { @@ -74,7 +74,7 @@ describe('XdIotTimeSeriesService', () => { from: faker.date.past(), to: faker.date.recent(), limit: faker.number.int(), - select: [ 'flow', 'pressure' ], + select: ['flow', 'pressure'], }; const assetId = faker.string.uuid(); const propertySetName = faker.lorem.word(1); @@ -103,55 +103,55 @@ describe('XdIotTimeSeriesService', () => { }); }); - describe('isLocalSession', () => { - it('should return true for isLocalSession when apiKey and apiUrl are undefined', () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - insightHubOptions.apiKey = null; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - insightHubOptions.apiUrl = null; + describe('isLocalSession', () => { + it('should return true for isLocalSession when apiKey and apiUrl are undefined', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + insightHubOptions.apiKey = null; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + insightHubOptions.apiUrl = null; - expect(service.isLocalSession()).toBe(true); - }); + expect(service.isLocalSession()).toBe(true); + }); - it('should return true for isLocalSession when apiKey and apiUrl are empty strings', () => { - insightHubOptions.apiKey = ''; - insightHubOptions.apiUrl = ''; + it('should return true for isLocalSession when apiKey and apiUrl are empty strings', () => { + insightHubOptions.apiKey = ''; + insightHubOptions.apiUrl = ''; - expect(service.isLocalSession()).toBe(true); - }); + expect(service.isLocalSession()).toBe(true); + }); - it('should return true for isLocalSession when apiKey is defined and apiUrl is undefined', () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - insightHubOptions.apiUrl = null; + it('should return true for isLocalSession when apiKey is defined and apiUrl is undefined', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + insightHubOptions.apiUrl = null; - expect(service.isLocalSession()).toBe(true); - }); + expect(service.isLocalSession()).toBe(true); + }); - it('should return true for isLocalSession when apiKey is undefined and apiUrl is defined', () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - insightHubOptions.apiKey = null; + it('should return true for isLocalSession when apiKey is undefined and apiUrl is defined', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + insightHubOptions.apiKey = null; - expect(service.isLocalSession()).toBe(true); - }); + expect(service.isLocalSession()).toBe(true); + }); - it('should return true for isLocalSession when apiKey is defined and apiUrl is an empty string', () => { - insightHubOptions.apiUrl = ''; + it('should return true for isLocalSession when apiKey is defined and apiUrl is an empty string', () => { + insightHubOptions.apiUrl = ''; - expect(service.isLocalSession()).toBe(true); - }); + expect(service.isLocalSession()).toBe(true); + }); - it('should return true for isLocalSession when apiKey is an empty string and apiUrl is defined', () => { - insightHubOptions.apiKey = ''; + it('should return true for isLocalSession when apiKey is an empty string and apiUrl is defined', () => { + insightHubOptions.apiKey = ''; - expect(service.isLocalSession()).toBe(true); - }); + expect(service.isLocalSession()).toBe(true); + }); - it('should return false for isLocalSession when apiKey and apiUrl are defined', () => { - expect(service.isLocalSession()).toBe(false); - }); - }); + it('should return false for isLocalSession when apiKey and apiUrl are defined', () => { + expect(service.isLocalSession()).toBe(false); + }); + }); }); From bd6d5bbacf7636fd1b22e32ba18e3890e29ed77d Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 16:30:29 +0200 Subject: [PATCH 16/21] feat(facilities-backend-metrics): integrate metrics lib Co-authored-by: Jonas --- .../facilities/backend/metrics/.eslintrc.json | 18 +++++ libs/facilities/backend/metrics/README.md | 3 + .../facilities/backend/metrics/jest.config.ts | 11 +++ libs/facilities/backend/metrics/project.json | 16 ++++ libs/facilities/backend/metrics/src/index.ts | 1 + .../src/lib/controller/metrics.controller.ts | 27 +++++++ .../lib/facilities-backend-metrics.module.ts | 14 ++++ .../src/lib/services/metrics.service.ts | 75 +++++++++++++++++++ libs/facilities/backend/metrics/tsconfig.json | 22 ++++++ .../backend/metrics/tsconfig.lib.json | 16 ++++ .../backend/metrics/tsconfig.spec.json | 9 +++ 11 files changed, 212 insertions(+) create mode 100644 libs/facilities/backend/metrics/.eslintrc.json create mode 100644 libs/facilities/backend/metrics/README.md create mode 100644 libs/facilities/backend/metrics/jest.config.ts create mode 100644 libs/facilities/backend/metrics/project.json create mode 100644 libs/facilities/backend/metrics/src/index.ts create mode 100644 libs/facilities/backend/metrics/src/lib/controller/metrics.controller.ts create mode 100644 libs/facilities/backend/metrics/src/lib/facilities-backend-metrics.module.ts create mode 100644 libs/facilities/backend/metrics/src/lib/services/metrics.service.ts create mode 100644 libs/facilities/backend/metrics/tsconfig.json create mode 100644 libs/facilities/backend/metrics/tsconfig.lib.json create mode 100644 libs/facilities/backend/metrics/tsconfig.spec.json diff --git a/libs/facilities/backend/metrics/.eslintrc.json b/libs/facilities/backend/metrics/.eslintrc.json new file mode 100644 index 00000000..274cb3ea --- /dev/null +++ b/libs/facilities/backend/metrics/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/facilities/backend/metrics/README.md b/libs/facilities/backend/metrics/README.md new file mode 100644 index 00000000..318a605e --- /dev/null +++ b/libs/facilities/backend/metrics/README.md @@ -0,0 +1,3 @@ +# facilities-backend-metrics + +This library provides a set of classes and functions to collect and report metrics from the backend services. diff --git a/libs/facilities/backend/metrics/jest.config.ts b/libs/facilities/backend/metrics/jest.config.ts new file mode 100644 index 00000000..65672883 --- /dev/null +++ b/libs/facilities/backend/metrics/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'facilities-backend-metrics', + preset: '../../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../../coverage/libs/facilities/backend/metrics', +}; diff --git a/libs/facilities/backend/metrics/project.json b/libs/facilities/backend/metrics/project.json new file mode 100644 index 00000000..d167801e --- /dev/null +++ b/libs/facilities/backend/metrics/project.json @@ -0,0 +1,16 @@ +{ + "name": "facilities-backend-metrics", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/facilities/backend/metrics/src", + "projectType": "library", + "tags": ["domain:facilities", "kind:backend", "type:feature"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/facilities/backend/metrics/jest.config.ts" + } + } + } +} diff --git a/libs/facilities/backend/metrics/src/index.ts b/libs/facilities/backend/metrics/src/index.ts new file mode 100644 index 00000000..6ac890ca --- /dev/null +++ b/libs/facilities/backend/metrics/src/index.ts @@ -0,0 +1 @@ +export * from './lib/facilities-backend-metrics.module'; diff --git a/libs/facilities/backend/metrics/src/lib/controller/metrics.controller.ts b/libs/facilities/backend/metrics/src/lib/controller/metrics.controller.ts new file mode 100644 index 00000000..e3f73f20 --- /dev/null +++ b/libs/facilities/backend/metrics/src/lib/controller/metrics.controller.ts @@ -0,0 +1,27 @@ +import { Controller, Get, Param } from '@nestjs/common'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { ESwaggerTag } from 'common-backend-swagger'; +import { GetTimeSeriesParamsDto } from 'facilities-backend-timeseries'; + +import { XdMetricsService } from '../services/metrics.service'; + +@ApiTags(ESwaggerTag.METRICS) +@Controller('metrics') +export class XdMetricsController { + constructor(private readonly metricsService: XdMetricsService) { + + } + + /** + * Get the metrics for the asset + * + * @param params + */ + @Get(':assetId/:propertySetName') + @ApiOkResponse({ description: 'Returns metrics data for an asset' }) + public getMetricsForAsset( + @Param() params: GetTimeSeriesParamsDto, + ) { + return this.metricsService.getMetricsForAsset(params.assetId, params.propertySetName); + } +} diff --git a/libs/facilities/backend/metrics/src/lib/facilities-backend-metrics.module.ts b/libs/facilities/backend/metrics/src/lib/facilities-backend-metrics.module.ts new file mode 100644 index 00000000..af509c03 --- /dev/null +++ b/libs/facilities/backend/metrics/src/lib/facilities-backend-metrics.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { XdPrismaModule } from 'common-backend-prisma'; +import { XdTimeseriesModule } from 'facilities-backend-timeseries'; + +import { XdMetricsController } from './controller/metrics.controller'; +import { XdMetricsService } from './services/metrics.service'; + +@Module({ + imports: [ XdTimeseriesModule, XdPrismaModule ], + controllers: [ XdMetricsController ], + providers: [ XdMetricsService ], + exports: [ XdMetricsService ], +}) +export class XdMetricsModule {} diff --git a/libs/facilities/backend/metrics/src/lib/services/metrics.service.ts b/libs/facilities/backend/metrics/src/lib/services/metrics.service.ts new file mode 100644 index 00000000..006be8af --- /dev/null +++ b/libs/facilities/backend/metrics/src/lib/services/metrics.service.ts @@ -0,0 +1,75 @@ +import { checkPumpStatus } from '@frontend/facilities/backend/utils'; +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { PrismaService } from 'common-backend-prisma'; +import { XdTimeseriesService } from 'facilities-backend-timeseries'; +import { ITimeSeriesPumpReport } from 'facilities-shared-models'; +import { from, map, switchMap } from 'rxjs'; +import dayjs = require('dayjs'); +import { pick } from 'lodash'; + +@Injectable() +export class XdMetricsService { + constructor( + @Inject(forwardRef(() => PrismaService)) + private readonly prismaService: PrismaService, + + private readonly timeSeriesService: XdTimeseriesService + ) { + } + + /** + * Get the metrics for the asset + * + * @param assetId + * @param propertySetName + */ + public getMetricsForAsset(assetId: string, propertySetName: string) { + return this.timeSeriesService.getTimeSeries({assetId, propertySetName, from: dayjs().subtract(60, 'minute').toDate()}) + .pipe( + switchMap((data) => { + return this.upsertMetrics(assetId, propertySetName, data) + }) + ) + } + + /** + * Calculates and upserts the metrics for the asset + * + * @param assetId + * @param propertySetName + * @param data + * @private + */ + private upsertMetrics(assetId: string, propertySetName: string, data: unknown): any { + switch (propertySetName) { + case 'PumpData': { + const { status, indicatorMsg, metrics } = checkPumpStatus( + data as unknown as ITimeSeriesPumpReport[], + ); + return from(this.prismaService.metrics.deleteMany({ + where: { + assetId: assetId, + } + })).pipe( + switchMap(() => { + return this.prismaService.asset.update({ + where: { + assetId, + }, + data: { + status, + indicatorMsg, + metrics: { + create: metrics + } + }, + }).metrics(); + }), + map((metrics) => metrics.map((m) => ({...pick(m, [ "name", "min", "max", "variance", "standardDeviation", "coefficientOfVariation", "mean" ])}))) + ) + } + } + } + + +} diff --git a/libs/facilities/backend/metrics/tsconfig.json b/libs/facilities/backend/metrics/tsconfig.json new file mode 100644 index 00000000..914bbe84 --- /dev/null +++ b/libs/facilities/backend/metrics/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/facilities/backend/metrics/tsconfig.lib.json b/libs/facilities/backend/metrics/tsconfig.lib.json new file mode 100644 index 00000000..4a5af54f --- /dev/null +++ b/libs/facilities/backend/metrics/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "target": "es2021", + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/facilities/backend/metrics/tsconfig.spec.json b/libs/facilities/backend/metrics/tsconfig.spec.json new file mode 100644 index 00000000..33157e0a --- /dev/null +++ b/libs/facilities/backend/metrics/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} From 5e782f1657d292c2cee0691d302b0267926f52b4 Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 16:31:08 +0200 Subject: [PATCH 17/21] feat(facilities-backend-metrics): integrate metrics lib Co-authored-by: Jonas --- .../src/lib/controller/metrics.controller.ts | 26 ++-- .../lib/facilities-backend-metrics.module.ts | 8 +- .../src/lib/services/metrics.service.ts | 138 ++++++++++-------- 3 files changed, 93 insertions(+), 79 deletions(-) diff --git a/libs/facilities/backend/metrics/src/lib/controller/metrics.controller.ts b/libs/facilities/backend/metrics/src/lib/controller/metrics.controller.ts index e3f73f20..13d899bd 100644 --- a/libs/facilities/backend/metrics/src/lib/controller/metrics.controller.ts +++ b/libs/facilities/backend/metrics/src/lib/controller/metrics.controller.ts @@ -8,20 +8,16 @@ import { XdMetricsService } from '../services/metrics.service'; @ApiTags(ESwaggerTag.METRICS) @Controller('metrics') export class XdMetricsController { - constructor(private readonly metricsService: XdMetricsService) { + constructor(private readonly metricsService: XdMetricsService) {} - } - - /** - * Get the metrics for the asset - * - * @param params - */ - @Get(':assetId/:propertySetName') - @ApiOkResponse({ description: 'Returns metrics data for an asset' }) - public getMetricsForAsset( - @Param() params: GetTimeSeriesParamsDto, - ) { - return this.metricsService.getMetricsForAsset(params.assetId, params.propertySetName); - } + /** + * Get the metrics for the asset + * + * @param params + */ + @Get(':assetId/:propertySetName') + @ApiOkResponse({ description: 'Returns metrics data for an asset' }) + public getMetricsForAsset(@Param() params: GetTimeSeriesParamsDto) { + return this.metricsService.getMetricsForAsset(params.assetId, params.propertySetName); + } } diff --git a/libs/facilities/backend/metrics/src/lib/facilities-backend-metrics.module.ts b/libs/facilities/backend/metrics/src/lib/facilities-backend-metrics.module.ts index af509c03..bcf348d3 100644 --- a/libs/facilities/backend/metrics/src/lib/facilities-backend-metrics.module.ts +++ b/libs/facilities/backend/metrics/src/lib/facilities-backend-metrics.module.ts @@ -6,9 +6,9 @@ import { XdMetricsController } from './controller/metrics.controller'; import { XdMetricsService } from './services/metrics.service'; @Module({ - imports: [ XdTimeseriesModule, XdPrismaModule ], - controllers: [ XdMetricsController ], - providers: [ XdMetricsService ], - exports: [ XdMetricsService ], + imports: [XdTimeseriesModule, XdPrismaModule], + controllers: [XdMetricsController], + providers: [XdMetricsService], + exports: [XdMetricsService], }) export class XdMetricsModule {} diff --git a/libs/facilities/backend/metrics/src/lib/services/metrics.service.ts b/libs/facilities/backend/metrics/src/lib/services/metrics.service.ts index 006be8af..09fec0f5 100644 --- a/libs/facilities/backend/metrics/src/lib/services/metrics.service.ts +++ b/libs/facilities/backend/metrics/src/lib/services/metrics.service.ts @@ -9,67 +9,85 @@ import { pick } from 'lodash'; @Injectable() export class XdMetricsService { - constructor( - @Inject(forwardRef(() => PrismaService)) - private readonly prismaService: PrismaService, + constructor( + @Inject(forwardRef(() => PrismaService)) + private readonly prismaService: PrismaService, - private readonly timeSeriesService: XdTimeseriesService - ) { - } - - /** - * Get the metrics for the asset - * - * @param assetId - * @param propertySetName - */ - public getMetricsForAsset(assetId: string, propertySetName: string) { - return this.timeSeriesService.getTimeSeries({assetId, propertySetName, from: dayjs().subtract(60, 'minute').toDate()}) - .pipe( - switchMap((data) => { - return this.upsertMetrics(assetId, propertySetName, data) - }) - ) - } - - /** - * Calculates and upserts the metrics for the asset - * - * @param assetId - * @param propertySetName - * @param data - * @private - */ - private upsertMetrics(assetId: string, propertySetName: string, data: unknown): any { - switch (propertySetName) { - case 'PumpData': { - const { status, indicatorMsg, metrics } = checkPumpStatus( - data as unknown as ITimeSeriesPumpReport[], - ); - return from(this.prismaService.metrics.deleteMany({ - where: { - assetId: assetId, - } - })).pipe( - switchMap(() => { - return this.prismaService.asset.update({ - where: { - assetId, - }, - data: { - status, - indicatorMsg, - metrics: { - create: metrics - } - }, - }).metrics(); - }), - map((metrics) => metrics.map((m) => ({...pick(m, [ "name", "min", "max", "variance", "standardDeviation", "coefficientOfVariation", "mean" ])}))) - ) - } - } - } + private readonly timeSeriesService: XdTimeseriesService, + ) {} + /** + * Get the metrics for the asset + * + * @param assetId + * @param propertySetName + */ + public getMetricsForAsset(assetId: string, propertySetName: string) { + return this.timeSeriesService + .getTimeSeries({ + assetId, + propertySetName, + from: dayjs().subtract(60, 'minute').toDate(), + }) + .pipe( + switchMap((data) => { + return this.upsertMetrics(assetId, propertySetName, data); + }), + ); + } + /** + * Calculates and upserts the metrics for the asset + * + * @param assetId + * @param propertySetName + * @param data + * @private + */ + private upsertMetrics(assetId: string, propertySetName: string, data: unknown): any { + switch (propertySetName) { + case 'PumpData': { + const { status, indicatorMsg, metrics } = checkPumpStatus( + data as unknown as ITimeSeriesPumpReport[], + ); + return from( + this.prismaService.metrics.deleteMany({ + where: { + assetId: assetId, + }, + }), + ).pipe( + switchMap(() => { + return this.prismaService.asset + .update({ + where: { + assetId, + }, + data: { + status, + indicatorMsg, + metrics: { + create: metrics, + }, + }, + }) + .metrics(); + }), + map((metrics) => + metrics.map((m) => ({ + ...pick(m, [ + 'name', + 'min', + 'max', + 'variance', + 'standardDeviation', + 'coefficientOfVariation', + 'mean', + ]), + })), + ), + ); + } + } + } } From 64d3661c5f9274192d4ea577e47e5fee5555141c Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 16:32:15 +0200 Subject: [PATCH 18/21] feat(facilities-backend-timeseries): services Co-authored-by: Jonas --- .../backend/timeseries/src/index.ts | 2 +- .../controller/timeseries.controller.spec.ts | 85 +------ .../backend/timeseries/src/lib/index.ts | 5 + .../lib/services/timeseries.service.spec.ts | 106 ++++---- .../src/lib/services/timeseries.service.ts | 229 +++++++++--------- 5 files changed, 176 insertions(+), 251 deletions(-) create mode 100644 libs/facilities/backend/timeseries/src/lib/index.ts diff --git a/libs/facilities/backend/timeseries/src/index.ts b/libs/facilities/backend/timeseries/src/index.ts index a8924ea1..f41a696f 100644 --- a/libs/facilities/backend/timeseries/src/index.ts +++ b/libs/facilities/backend/timeseries/src/index.ts @@ -1 +1 @@ -export * from './lib/timeseries.module'; +export * from './lib'; diff --git a/libs/facilities/backend/timeseries/src/lib/controller/timeseries.controller.spec.ts b/libs/facilities/backend/timeseries/src/lib/controller/timeseries.controller.spec.ts index 7a3cb400..8095b6fe 100644 --- a/libs/facilities/backend/timeseries/src/lib/controller/timeseries.controller.spec.ts +++ b/libs/facilities/backend/timeseries/src/lib/controller/timeseries.controller.spec.ts @@ -30,6 +30,12 @@ describe('TimeseriesController ', () => { { time: faker.date.recent(), test: faker.string.sample() }, ] as ITimeSeriesDataItemResponse[]) as Observable, ), + getTimeSeries: jest.fn().mockReturnValue( + of([ + { time: faker.date.recent(), test: faker.string.sample() }, + { time: faker.date.recent(), test: faker.string.sample() }, + ] as ITimeSeriesDataItemResponse[]) as Observable, + ), getTimeSeriesFromDB: jest.fn().mockReturnValue( of([ { time: faker.date.recent(), test: faker.string.sample() }, @@ -40,7 +46,7 @@ describe('TimeseriesController ', () => { }; const module = await Test.createTestingModule({ - controllers: [ XdTimeseriesController ], + controllers: [XdTimeseriesController], providers: [ { provide: XdTimeseriesService, @@ -85,7 +91,7 @@ describe('TimeseriesController ', () => { const from = faker.date.recent(); const to = faker.date.recent(); const limit = faker.number.int(10); - const select = [ 'test' ]; + const select = ['test']; const sort = ESortOrder.ASCENDING; const latestValue = true; @@ -94,66 +100,10 @@ describe('TimeseriesController ', () => { { time: faker.date.recent(), test: faker.string.sample() }, ] as ITimeSeriesDataItemResponse[]; - const spy = jest - .spyOn(service, 'getTimeSeriesFromDB') - .mockReturnValue(of(returnValue) as Observable); - - const result = await firstValueFrom( - controller.getTimeSeries( - { - assetId, - propertySetName, - }, - { - from, - to, - limit, - select, - sort, - latestValue, - local: true, - }, - ), + jest.spyOn(service, 'getTimeSeries').mockReturnValue( + of(returnValue) as Observable, ); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledWith({ - assetId, - propertySetName, - from, - to, - limit, - select, - sort, - latestValue, - }); - expect(result).toEqual(returnValue); - }); - - it(' should call getTimeSeriesFromApi when local is false', async () => { - const assetId = faker.string.uuid(); - const propertySetName = faker.string.uuid(); - const from = faker.date.recent(); - const to = faker.date.recent(); - const limit = faker.number.int(10); - const select = [ 'test' ]; - const sort = ESortOrder.ASCENDING; - const latestValue = true; - const local = false; - - const returnValue = [ - { time: faker.date.recent(), test: faker.string.sample() }, - { time: faker.date.recent(), test: faker.string.sample() }, - ] as ITimeSeriesDataItemResponse[]; - - const spyApi = jest - .spyOn(service, 'getTimeSeriesFromApi') - .mockReturnValue(of(returnValue) as Observable); - - const spyDb = jest - .spyOn(service, 'getTimeSeriesFromDB') - .mockReturnValue(of(returnValue) as Observable); - const result = await firstValueFrom( controller.getTimeSeries( { @@ -167,24 +117,11 @@ describe('TimeseriesController ', () => { select, sort, latestValue, - local, + local: true, }, ), ); - expect(spyApi).toHaveBeenCalledTimes(1); - expect(spyApi).toHaveBeenCalledWith({ - assetId, - propertySetName, - from, - to, - limit, - select, - sort, - latestValue, - }); expect(result).toEqual(returnValue); - - expect(spyDb).toHaveBeenCalledTimes(0); }); }); diff --git a/libs/facilities/backend/timeseries/src/lib/index.ts b/libs/facilities/backend/timeseries/src/lib/index.ts new file mode 100644 index 00000000..3d6d8c89 --- /dev/null +++ b/libs/facilities/backend/timeseries/src/lib/index.ts @@ -0,0 +1,5 @@ +export * from './dto'; +export * from './services'; + +export * from './timeseries.module'; + diff --git a/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.spec.ts b/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.spec.ts index abd808ab..9734a9b6 100644 --- a/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.spec.ts +++ b/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.spec.ts @@ -7,7 +7,6 @@ import { XdIotTimeSeriesService } from 'common-backend-insight-hub'; import { XdTokenManagerService } from 'common-backend-insight-hub'; import { PrismaService } from 'common-backend-prisma'; import { ESortOrder, IGetTimeSeriesParams, IGetTimeseriesQuery } from 'facilities-shared-models'; -import { omit } from 'lodash'; import { lastValueFrom, of } from 'rxjs'; import { XdTimeseriesService } from './timeseries.service'; @@ -32,6 +31,12 @@ describe('TimeseriesService', () => { data: JSON.stringify({ test: 'test', test2: 'test2' }), }, ]), + findFirst: jest.fn().mockImplementation(() => [ + { + time: new Date(), + data: JSON.stringify({ test: 'test', test2: 'test2' }), + }, + ]), upsert: jest.fn(), }, @@ -63,7 +68,7 @@ describe('TimeseriesService', () => { provide: XdIotTimeSeriesService, useValue: { getTimeSeriesData: jest.fn().mockReturnValue(of([])), - isLocalSession: jest.fn().mockReturnValue(true), + isLocalSession: jest.fn().mockReturnValue(true), }, }, { @@ -120,7 +125,7 @@ describe('TimeseriesService', () => { const findManySpy = jest .spyOn(prisma.timeSeriesDataItem, 'findMany') - .mockResolvedValue([ findManyResult ]); + .mockResolvedValue([findManyResult]); const params: IGetTimeSeriesParams = { assetId: findManyResult.timeSeriesItemAssetId, @@ -130,19 +135,13 @@ describe('TimeseriesService', () => { const result = await lastValueFrom( service.getTimeSeriesFromDB({ ...params, - select: [ 'flow', 'presure' ], + select: ['flow', 'presure'], }), ); expect(findManySpy).toHaveBeenCalledTimes(1); - expect(result).toEqual([ - { - time: findManyResult.time, - flow: flow, - presure: presure, - }, - ]); + expect(result[0]['flow']).toEqual(flow); }); it('should call selectKeysFromJSON only with the selected Props', async () => { @@ -159,7 +158,7 @@ describe('TimeseriesService', () => { const findManySpy = jest .spyOn(prisma.timeSeriesDataItem, 'findMany') - .mockResolvedValue([ findManyResult ]); + .mockResolvedValue([findManyResult]); const params: IGetTimeSeriesParams = { assetId: findManyResult.timeSeriesItemAssetId, @@ -167,7 +166,7 @@ describe('TimeseriesService', () => { }; const query: IGetTimeseriesQuery = { - select: [ 'flow' ], + select: ['flow'], }; const result = await lastValueFrom( @@ -196,14 +195,13 @@ describe('TimeseriesService', () => { .spyOn(prisma.timeSeriesDataItem, 'findMany') .mockResolvedValue([]); - const params: IGetTimeSeriesParams = { assetId: faker.string.uuid(), propertySetName: faker.string.sample(), }; const query: IGetTimeseriesQuery = { - select: [ 'flow' ], + select: ['flow'], }; await lastValueFrom( @@ -213,13 +211,12 @@ describe('TimeseriesService', () => { }), ); - expect(findManySpy).toHaveBeenCalledTimes(0); expect(getTimeSeriesDataSpy).toHaveBeenCalledWith( params.assetId, params.propertySetName, - query + query, ); await lastValueFrom( @@ -233,58 +230,47 @@ describe('TimeseriesService', () => { expect(findManySpy).toHaveBeenCalledTimes(1); }); - it('should use local db when api iot service decides its a local session', async ()=> { - const getTimeSeriesDataSpy = jest - .spyOn(iothub, 'getTimeSeriesData') - .mockReturnValue(of([])); - - const findManySpy = jest - .spyOn(prisma.timeSeriesDataItem, 'findMany') - .mockResolvedValue([]); - - const isLocalSessionSpy = jest - .spyOn(iothub, 'isLocalSession') - - - const params: IGetTimeSeriesParams = { - assetId: faker.string.uuid(), - propertySetName: faker.string.sample(), - }; + it('should use local db when api iot service decides its a local session', async () => { + const getTimeSeriesDataSpy = jest + .spyOn(iothub, 'getTimeSeriesData') + .mockReturnValue(of([])); - const query: IGetTimeseriesQuery = { - select: [ 'flow' ], - }; + const findManySpy = jest + .spyOn(prisma.timeSeriesDataItem, 'findMany') + .mockResolvedValue([]); - isLocalSessionSpy.mockReturnValue(false) + const isLocalSessionSpy = jest.spyOn(iothub, 'isLocalSession'); - await lastValueFrom( - service.getTimeSeries({ - ...params, - ...query, - }), - ); + const params: IGetTimeSeriesParams = { + assetId: faker.string.uuid(), + propertySetName: faker.string.sample(), + }; + const query: IGetTimeseriesQuery = { + select: ['flow'], + }; - expect(findManySpy).toHaveBeenCalledTimes(0); + isLocalSessionSpy.mockReturnValue(false); - expect(getTimeSeriesDataSpy).toHaveBeenCalledWith( - params.assetId, - params.propertySetName, - query - ); + await lastValueFrom( + service.getTimeSeries({ + ...params, + ...query, + }), + ); - isLocalSessionSpy.mockReturnValue(true) + isLocalSessionSpy.mockReturnValue(true); - await lastValueFrom( - service.getTimeSeries({ - ...params, - ...query, - }), - ); + await lastValueFrom( + service.getTimeSeries({ + ...params, + ...query, + }), + ); - expect(getTimeSeriesDataSpy).toHaveBeenCalledTimes(1); - expect(findManySpy).toHaveBeenCalledTimes(1); - }) + expect(getTimeSeriesDataSpy).toHaveBeenCalledTimes(1); + expect(findManySpy).toHaveBeenCalledTimes(1); + }); it('should use the correct args to query the time series data', async () => { const findManySpy = jest diff --git a/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.ts b/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.ts index 33c3439b..5294297e 100644 --- a/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.ts +++ b/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.ts @@ -1,18 +1,15 @@ -import { checkPumpStatus } from '@frontend/facilities/backend/utils'; import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; import { ETimeSeriesOrdering, XdIotTimeSeriesService } from 'common-backend-insight-hub'; import { PrismaService } from 'common-backend-prisma'; import { - IGetTimeSeriesParams, - IGetTimeseriesQuery, - ITimeSeriesDataItemResponse, - ITimeSeriesItemResponse + IGetTimeSeriesParams, + IGetTimeseriesQuery, + ITimeSeriesDataItemResponse, + ITimeSeriesItemResponse, } from 'facilities-shared-models'; -import { catchError, from, map, Observable, switchMap } from 'rxjs'; import { omit } from 'lodash'; +import { from, map, Observable, switchMap } from 'rxjs'; import dayjs = require('dayjs'); -import { Dayjs } from 'dayjs'; - @Injectable() export class XdTimeseriesService { @@ -23,130 +20,130 @@ export class XdTimeseriesService { private readonly iotTimeSeriesService: XdIotTimeSeriesService, ) {} - /** - * Acts as a gateway to get the time series data from either the API or the DB. - * - * @param args - the query and parameters based arguments to get the time series data - */ - public getTimeSeries(args: IGetTimeSeriesParams & IGetTimeseriesQuery){ - return !this.iotTimeSeriesService.isLocalSession()? - this.getTimeSeriesFromDB(args): - this.getTimeSeriesFromApi(args); - } + /** + * Acts as a gateway to get the time series data from either the API or the DB. + * + * @param args - the query and parameters based arguments to get the time series data + */ + public getTimeSeries(args: IGetTimeSeriesParams & IGetTimeseriesQuery) { + const result$ = this.iotTimeSeriesService.isLocalSession() + ? this.getTimeSeriesFromDB(args) + : this.getTimeSeriesFromApi(args); + + return result$; + } /** * Get timeseries data based on the assetId and propertySetName from the API - * - * @param args - the query and parameters based arguments to get the time series data + * + * @param args - the query and parameters based arguments to get the time series data */ public getTimeSeriesFromApi( args: IGetTimeSeriesParams & IGetTimeseriesQuery, ): Observable { const { assetId, propertySetName, sort, ...params } = args; + return this.iotTimeSeriesService + .getTimeSeriesData< + any, + { + _time: string; + [key: string]: any; + }[] + >(assetId, propertySetName, { + ...params, + // Todo: Fix this in a future PR + sort: sort as unknown as ETimeSeriesOrdering, + }) + .pipe( + map((items) => { + return items.map((item) => ({ + time: new Date(item._time), + ...omit(item, '_time'), + })); + }), + ); + } + + private findFirstTime(assetId: string, propertySetName: string) { + return from( + this.prismaService.timeSeriesDataItem.findFirst({ + where: { + timeSeriesItemAssetId: assetId, + timeSeriesItemPropertySetName: propertySetName, + }, + orderBy: { + time: 'desc', + }, + }), + ); + } - return this.iotTimeSeriesService - .getTimeSeriesData< - any, - { - _time: string; - [key: string]: any; - }[] - >(assetId, propertySetName, { - ...params, - // Todo: Fix this in a future PR - sort: sort as unknown as ETimeSeriesOrdering, - }).pipe( - map((items) => { - return items.map( - (item) => ({ - time: new Date(item._time), - ...omit(item, "_time") - }) - ) - } - ) - ) - } - - private findFirstTime(assetId: string, propertySetName: string) { - return from( - this.prismaService.timeSeriesDataItem.findFirst({ - where: { - timeSeriesItemAssetId: assetId, - timeSeriesItemPropertySetName: propertySetName, - }, - orderBy: { - time: 'desc', - }, - })); - } - - private normalizeTimes(time: Date, from?: Date, to?: Date) { - let normalizedFromTime: Date | undefined; - let normalizedToTime: Date | undefined; - const timeDifference = dayjs().diff(time, 'millisecond', true); - - if (from) { - normalizedFromTime = dayjs(from).subtract(timeDifference, 'millisecond').toDate(); - } - if (to) { - normalizedToTime = dayjs(to).subtract(timeDifference, 'millisecond').toDate(); - } - - return { normalizedFromTime, normalizedToTime, timeDifference }; - } + private normalizeTimes(time: Date, from?: Date, to?: Date) { + let normalizedFromTime: Date | undefined; + let normalizedToTime: Date | undefined; + const timeDifference = dayjs().diff(time, 'millisecond', true); + + if (from) { + normalizedFromTime = dayjs(from).subtract(timeDifference, 'millisecond').toDate(); + } + if (to) { + normalizedToTime = dayjs(to).subtract(timeDifference, 'millisecond').toDate(); + } + + return { normalizedFromTime, normalizedToTime, timeDifference }; + } /** * Get timeseries data based on the assetId and propertySetName from the DB - * - * @param args - the query and parameters based arguments to get the time series data + * + * @param args - the query and parameters based arguments to get the time series data */ public getTimeSeriesFromDB( args: IGetTimeSeriesParams & IGetTimeseriesQuery, ): Observable { - const { assetId, propertySetName } = args - - - return this.findFirstTime(assetId, propertySetName).pipe( - switchMap((item) => { - if (!item) { - throw new HttpException('timeSeriesItem not found', HttpStatus.NOT_FOUND); - } - - const { normalizedFromTime, normalizedToTime, timeDifference } = this.normalizeTimes(item.time, args.from, args.to); - - return from( - this.prismaService.timeSeriesDataItem.findMany({ - where: { - timeSeriesItemAssetId: assetId, - timeSeriesItemPropertySetName: propertySetName, - time: { - gte: normalizedFromTime, - lte: normalizedToTime, - }, - }, - take: args.limit, - orderBy: { - time: args.sort, - }, - }) - ).pipe( - map((result) => ({ result, timeDifference })) - ); - }), - map(({ result, timeDifference }) => { - if (!Array.isArray(result)) { - throw new HttpException('Unexpected result format', HttpStatus.INTERNAL_SERVER_ERROR); - } - - return result.map((item) => ({ - time: dayjs(item.time).add(timeDifference, 'millisecond').toDate(), - ...this.prismaService.selectKeysFromJSON(item.data, args.select), - })); - }), - ); - } - + const { assetId, propertySetName } = args; + + return this.findFirstTime(assetId, propertySetName).pipe( + switchMap((item) => { + if (!item) { + throw new HttpException('timeSeriesItem not found', HttpStatus.NOT_FOUND); + } + + const { normalizedFromTime, normalizedToTime, timeDifference } = + this.normalizeTimes(item.time, args.from, args.to); + + return from( + this.prismaService.timeSeriesDataItem.findMany({ + where: { + timeSeriesItemAssetId: assetId, + timeSeriesItemPropertySetName: propertySetName, + time: { + gte: normalizedFromTime, + lte: normalizedToTime, + }, + }, + take: args.limit, + orderBy: { + time: args.sort, + }, + }), + ).pipe(map((result) => ({ result, timeDifference }))); + }), + map(({ result, timeDifference }) => { + if (!Array.isArray(result)) { + throw new HttpException( + 'Unexpected result format', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + return result.map((item) => ({ + time: dayjs(item.time).add(timeDifference, 'millisecond').toDate(), + ...this.prismaService.selectKeysFromJSON(item.data, args.select), + })); + }), + ); + } // return from( // this.prismaService.timeSeriesDataItem.findMany({ From 15cb3e008f6a697ef7d65fe71fc4f33be0a5a412 Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 16:33:44 +0200 Subject: [PATCH 19/21] feat(facilities-frontend-domain): metrics request services Co-authored-by: Jonas --- .../facades/details.facade.spec.ts | 7 +++ .../lib/application/facades/details.facade.ts | 52 +++++++++++++++---- .../metrics-request.service.spec.ts | 52 +++++++++++++++++++ .../infrastructure/metrics-request.service.ts | 24 +++++++++ 4 files changed, 124 insertions(+), 11 deletions(-) create mode 100644 libs/facilities/frontend/domain/src/lib/infrastructure/metrics-request.service.spec.ts create mode 100644 libs/facilities/frontend/domain/src/lib/infrastructure/metrics-request.service.ts diff --git a/libs/facilities/frontend/domain/src/lib/application/facades/details.facade.spec.ts b/libs/facilities/frontend/domain/src/lib/application/facades/details.facade.spec.ts index fa785f26..9cb9dee5 100644 --- a/libs/facilities/frontend/domain/src/lib/application/facades/details.facade.spec.ts +++ b/libs/facilities/frontend/domain/src/lib/application/facades/details.facade.spec.ts @@ -2,6 +2,7 @@ import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; import { FacilitiesRequestService } from '../../infrastructure/facilities-request.service'; +import { MetricsRequestService } from '../../infrastructure/metrics-request.service'; import { TimeSeriesRequestService } from '../../infrastructure/timeseries-request.service'; import { XdDetailsFacade } from './details.facade'; @@ -29,6 +30,12 @@ describe('XdDetailsFacadeService', () => { getTimeSeriesDataItems: jest.fn().mockReturnValue(of([])), }, }, + { + provide: MetricsRequestService, + useValue: { + getMetrics: jest.fn().mockReturnValue(of([])), + }, + }, ], }); diff --git a/libs/facilities/frontend/domain/src/lib/application/facades/details.facade.ts b/libs/facilities/frontend/domain/src/lib/application/facades/details.facade.ts index 6927286f..f158c107 100644 --- a/libs/facilities/frontend/domain/src/lib/application/facades/details.facade.ts +++ b/libs/facilities/frontend/domain/src/lib/application/facades/details.facade.ts @@ -1,8 +1,10 @@ import { inject, Injectable } from '@angular/core'; import { faker } from '@faker-js/faker'; -import { lastValueFrom, map, tap } from 'rxjs'; +import * as dayjs from 'dayjs'; +import { map } from 'rxjs'; import { FacilitiesRequestService } from '../../infrastructure/facilities-request.service'; +import { MetricsRequestService } from '../../infrastructure/metrics-request.service'; import { TimeSeriesRequestService } from '../../infrastructure/timeseries-request.service'; /** @@ -12,6 +14,7 @@ import { TimeSeriesRequestService } from '../../infrastructure/timeseries-reques export class XdDetailsFacade { private readonly _facilitiesService = inject(FacilitiesRequestService); private readonly _timeseriesService = inject(TimeSeriesRequestService); + private readonly _metricsService = inject(MetricsRequestService); /** * Get facility @@ -29,12 +32,12 @@ export class XdDetailsFacade { 'water-plant', 'truck', ]), - cases: timeSeriesItem.cases, + cases: timeSeriesItem.cases, heading: timeSeriesItem.name, subheading: timeSeriesItem.description, status: timeSeriesItem.status, - metrics: timeSeriesItem.metrics, - indicatorMsg: timeSeriesItem.indicatorMsg, + metrics: timeSeriesItem.metrics, + indicatorMsg: timeSeriesItem.indicatorMsg, pumps: faker.number.int({ min: 0, max: 99 }), location: timeSeriesItem.location, }; @@ -42,6 +45,16 @@ export class XdDetailsFacade { ); } + /** + * Returns the metrics for the asset + * + * @param assetId + * @param propertySetName + */ + public getMetrics(assetId: string, propertySetName: string) { + return this._metricsService.getMetrics({ assetId, propertySetName }); + } + /** * Get a list of all the available timeSeries properties * @param assetId The asset id. @@ -51,15 +64,32 @@ export class XdDetailsFacade { } /** - * Get the specific data of a time series property of a facility + * Get the specific data of a time series property of a facility of the last 28 minutes + * * @param assetId The asset id. - * @param propertySetName The property set name for which we will get the data. - * @param queryParams The query parameters. */ - public getTimeSeriesDataItems(assetId: string, propertySetName: string, queryParams: any) { - return this._timeseriesService.getTimeSeriesDataItems( - { assetId, propertySetName }, - queryParams, + public getPumpData(assetId: string) { + return this._timeseriesService.getTimeSeriesDataItems( + { assetId, propertySetName: 'PumpData' }, + { + from: dayjs().subtract(28, 'minute').toDate(), + to: dayjs().toDate(), + }, + ); + } + + /** + * Get the environment data of a facility of the last 24 hours + * + * @param assetId + */ + public getEnvironmentData(assetId: string) { + return this._timeseriesService.getTimeSeriesDataItems( + { assetId, propertySetName: 'Environment' }, + { + from: dayjs().subtract(24, 'hours').toDate(), + to: dayjs().toDate(), + }, ); } } diff --git a/libs/facilities/frontend/domain/src/lib/infrastructure/metrics-request.service.spec.ts b/libs/facilities/frontend/domain/src/lib/infrastructure/metrics-request.service.spec.ts new file mode 100644 index 00000000..0c78d07a --- /dev/null +++ b/libs/facilities/frontend/domain/src/lib/infrastructure/metrics-request.service.spec.ts @@ -0,0 +1,52 @@ +import { HttpClient } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { faker } from '@faker-js/faker'; +import { IGetTimeSeriesParams, IPumpMetrics } from 'facilities-shared-models'; +import { firstValueFrom, of } from 'rxjs'; + +import { MetricsRequestService } from './metrics-request.service'; +import { TimeSeriesRequestService } from './timeseries-request.service'; + +describe('MetricsService', () => { + let service: MetricsRequestService; + let httpClient: HttpClient; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ HttpClientTestingModule ], + providers: [ + TimeSeriesRequestService, + { + provide: HttpClient, + useValue: { + get: jest.fn(), + }, + }, + ], + }); + + service = TestBed.inject(MetricsRequestService); + httpClient = TestBed.inject(HttpClient); + }); + + describe('getMetrics', () => { + it('should fetch time series data', async () => { + const mockResponse: IPumpMetrics[] = []; + + const params: IGetTimeSeriesParams = { + assetId: faker.string.uuid(), + propertySetName: 'PumpData', + }; + + const spy = jest.spyOn(httpClient, 'get').mockReturnValue(of(mockResponse)); + + const result = await firstValueFrom(service.getMetrics(params)); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith( + `/api/metrics/${params.assetId}/${params.propertySetName}`, + ); + expect(result).toEqual(mockResponse); + }); + }); +}); diff --git a/libs/facilities/frontend/domain/src/lib/infrastructure/metrics-request.service.ts b/libs/facilities/frontend/domain/src/lib/infrastructure/metrics-request.service.ts new file mode 100644 index 00000000..8b386817 --- /dev/null +++ b/libs/facilities/frontend/domain/src/lib/infrastructure/metrics-request.service.ts @@ -0,0 +1,24 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { + IGetTimeSeriesParams, IPumpMetrics, +} from 'facilities-shared-models'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class MetricsRequestService { + private readonly _httpClient = inject(HttpClient); + private readonly _baseRoute = '/api/metrics'; + + /** + * Get the metrics for the asset + * + * @param params + */ + public getMetrics(params: IGetTimeSeriesParams): Observable { + return this._httpClient.get(`${this._baseRoute}/${params.assetId}/${params.propertySetName}`); + } + +} From 2f39557631a1f77722606cc9e4de757de6fe0be8 Mon Sep 17 00:00:00 2001 From: KonsumGandalf Date: Sat, 13 Jul 2024 16:36:30 +0200 Subject: [PATCH 20/21] feat(facilities-frontend-view): set metrics height Co-authored-by: Jonas --- .../lib/components/detail/detail.page.html | 213 +++++++------- .../src/lib/components/detail/detail.page.ts | 267 +++++++++--------- 2 files changed, 239 insertions(+), 241 deletions(-) diff --git a/libs/facilities/frontend/view/src/lib/components/detail/detail.page.html b/libs/facilities/frontend/view/src/lib/components/detail/detail.page.html index 5e77215e..47e11d59 100644 --- a/libs/facilities/frontend/view/src/lib/components/detail/detail.page.html +++ b/libs/facilities/frontend/view/src/lib/components/detail/detail.page.html @@ -1,25 +1,22 @@ @if (facility(); as facility) { -
- - @if(this.pumpChart(); as pumpChart){ -
- } - @if(this.envChart(); as envChart){ -
- } - @if(this.metricsChart(); as metricsChart){ -
- } +
+ @if(this.pumpChart(); as pumpChart){ +
+ } @if(this.envChart(); as envChart){ +
+ } @if(this.metricsChart(); as metricsChart){ +
+ } @@ -30,105 +27,117 @@ {{ facility.heading }} {{ facility.subheading }} - @if(facility.location?.streetAddress !== undefined && facility.location?.locality !== undefined && facility.location?.country !== undefined){ -

Location: {{ facility.location?.streetAddress }}, {{ facility.location?.postalCode }} {{ facility.location?.locality }}, {{ facility.location?.country }}

- } @else if(facility.location?.streetAddress !== undefined && facility.location?.locality !== undefined){ -

Location: {{ facility.location?.streetAddress }}, {{ facility.location?.postalCode }} {{ facility.location?.locality }}

- } @else if (facility.location?.country !== undefined) { - Location: {{ facility.location?.postalCode }} {{ facility.location?.country }} - } @else if (facility.location?.locality !== undefined) { -

Location: {{ facility.location?.postalCode}} {{ facility.location?.locality }}

- } @else { -

Location: Not specified

- } -
+ @if(facility.location?.streetAddress !== undefined && + facility.location?.locality !== undefined && facility.location?.country !== + undefined){ +

+ Location: {{ facility.location?.streetAddress }}, {{ + facility.location?.postalCode }} {{ facility.location?.locality }}, {{ + facility.location?.country }} +

+ } @else if(facility.location?.streetAddress !== undefined && + facility.location?.locality !== undefined){ +

+ Location: {{ facility.location?.streetAddress }}, {{ + facility.location?.postalCode }} {{ facility.location?.locality }} +

+ } @else if (facility.location?.country !== undefined) { Location: {{ + facility.location?.postalCode }} {{ facility.location?.country }} } @else if + (facility.location?.locality !== undefined) { +

+ Location: {{ facility.location?.postalCode}} {{ facility.location?.locality + }} +

+ } @else { +

Location: Not specified

+ } +
status:  {{ facility.status }} - {{ facility.indicatorMsg }} + {{ facility.indicatorMsg }}
- - - - - {{ facility.cases.length > 0 ? facility.cases.length : '' }} - - Open Cases - - {{ notificationText() }} - -
- @if (facility.cases.length > 0) { -
-
- - @for (caseItem of facility.cases.slice(0, facility.cases.length === 4 ? 4 : 3); - track caseItem.id; let i=$index){ - - Case with id {{ caseItem.id }} - - } -
- @if (facility.cases.length > 4 ) { - - Go to all Cases - - } -
- } - -
- - New Case - -
-
+ + + + + {{ facility.cases.length > 0 ? facility.cases.length : '' }} + + Open Cases + {{ notificationText() }} +
+ @if (facility.cases.length > 0) { +
+
+ + @for (caseItem of facility.cases.slice(0, facility.cases.length === 4 ? + 4 : 3); track caseItem.id; let i=$index){ + + Case with id {{ caseItem.id }} + + } +
+ @if (facility.cases.length > 4 ) { + Go to all Cases + } +
+ } - - +
+ + New Case + +
+
+
+
- - - - - - - {{ locked() ? 'Unlock the doors' : 'Lock the doors' }} - - - {{ locked() ? 'Currently the doors are locked' : - 'Currently the doors are unlocked' }} - + + + + + + + {{ locked() ? 'Unlock the doors' : 'Lock the doors' }} + + + {{ locked() ? 'Currently the doors are locked' : 'Currently the doors are + unlocked' }} + -
- - {{ locked() ? 'Unlock the doors' : 'Lock the doors' }} - -
-
-
+
+ + {{ locked() ? 'Unlock the doors' : 'Lock the doors' }} + +
+
+
- - - - - 80% - - Eco Score (SiGreen) - - According to SiGreen this facility has an eco score of 80% - - - -
+ + + + + 80% + + Eco Score (SiGreen) + + According to SiGreen this facility has an eco score of 80% + + + +
} diff --git a/libs/facilities/frontend/view/src/lib/components/detail/detail.page.ts b/libs/facilities/frontend/view/src/lib/components/detail/detail.page.ts index 1a8ecbd8..e19c615b 100644 --- a/libs/facilities/frontend/view/src/lib/components/detail/detail.page.ts +++ b/libs/facilities/frontend/view/src/lib/components/detail/detail.page.ts @@ -1,13 +1,13 @@ import { CommonModule, Location } from '@angular/common'; import { - ChangeDetectionStrategy, - Component, - computed, - inject, - OnInit, - Signal, - signal, - ViewEncapsulation, + ChangeDetectionStrategy, + Component, + computed, + inject, + OnInit, + Signal, + signal, + ViewEncapsulation, } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, RouterLink } from '@angular/router'; @@ -32,27 +32,26 @@ import { PUMP_METRICS_FULL_NAME_MAP } from './models/pump-metrics-full-name.map' @Component({ selector: 'lib-detail', standalone: true, - imports: [ CommonModule, IxModule, NgxEchartsModule, LockModalComponent, RouterLink ], + imports: [CommonModule, IxModule, NgxEchartsModule, LockModalComponent, RouterLink], templateUrl: './detail.page.html', styleUrl: './detail.page.scss', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) export class XdDetailPage implements OnInit { + protected readonly notificationText = computed(() => { + const facility = this.facility(); + if (!facility) return undefined; - protected readonly notificationText = computed(() => { - const facility = this.facility(); - if (!facility) return undefined; - - switch (facility.cases.length) { - case 0: - return 'There are no cases regarding this facility'; - case 1: - return 'There is one case regarding this facility'; - default: - return `There are ${facility.cases.length} cases regarding this facility`; - } - }) + switch (facility.cases.length) { + case 0: + return 'There are no cases regarding this facility'; + case 1: + return 'There is one case regarding this facility'; + default: + return `There are ${facility.cases.length} cases regarding this facility`; + } + }); protected theme = signal(convertThemeName(themeSwitcher.getCurrentTheme())); protected readonly locked = signal(true); @@ -62,32 +61,25 @@ export class XdDetailPage implements OnInit { private readonly _28MinutesAgo = new Date(this._currentTime.getTime() - 28 * 60 * 1000); private readonly _detailsFacade = inject(XdDetailsFacade); protected readonly facility = toSignal(this._detailsFacade.getFacility(this._assetId)); - protected readonly pumpData = toSignal( - this._detailsFacade.getTimeSeriesDataItems(this._assetId, 'PumpData', { - from: this._28MinutesAgo, - to: this._currentTime, - }), - ); - protected readonly envData = toSignal( - this._detailsFacade.getTimeSeriesDataItems(this._assetId, 'Environment', { - from: this._28MinutesAgo, - to: this._currentTime, - }), + protected readonly pumpData = toSignal(this._detailsFacade.getPumpData(this._assetId)); + protected readonly envData = toSignal(this._detailsFacade.getEnvironmentData(this._assetId)); + protected readonly metricsData = toSignal( + this._detailsFacade.getMetrics(this._assetId, 'PumpData'), ); private readonly defaultOptions: EChartsOption = { - tooltip: { - trigger: 'axis', - renderMode: 'auto', - axisPointer: { - axis: 'auto', - crossStyle: { - textStyle: { - precision: 2, - } - } - } - }, + tooltip: { + trigger: 'axis', + renderMode: 'auto', + axisPointer: { + axis: 'auto', + crossStyle: { + textStyle: { + precision: 2, + }, + }, + }, + }, xAxis: { type: 'time', name: 'Time', @@ -109,35 +101,35 @@ export class XdDetailPage implements OnInit { top: 80, }, }; - private readonly barChartOptions: EChartsOption = { - tooltip: { - trigger: 'axis', - renderMode: 'auto', - axisPointer: { - axis: 'auto', - crossStyle: { - textStyle: { - precision: 2, - } - } - } - }, - legend: { - top: 30, - left: 80, - }, - grid: { - top: 80, - }, - title: { - text: 'Pump Metrics', - left: 'center', - }, - yAxis: { - type: 'value', - nameLocation: 'middle', - }, - } + private readonly barChartOptions: EChartsOption = { + tooltip: { + trigger: 'axis', + renderMode: 'auto', + axisPointer: { + axis: 'auto', + crossStyle: { + textStyle: { + precision: 2, + }, + }, + }, + }, + legend: { + top: 30, + left: 80, + }, + grid: { + top: 80, + }, + title: { + text: 'Pump Metrics', + left: 'center', + }, + yAxis: { + type: 'value', + nameLocation: 'middle', + }, + }; private readonly pumpOptions: EChartsOption = { ...this.defaultOptions, title: { @@ -179,23 +171,22 @@ export class XdDetailPage implements OnInit { }; protected readonly pumpChart: Signal = computed(() => { const pumpData = this.pumpData(); - if (!pumpData) return undefined; - const pumpChart = { ...this.pumpOptions, }; + if (!pumpData) return pumpChart; + if (!pumpChart.series || !(pumpChart.series instanceof Array)) return undefined; - pumpChart.series[0].data = pumpData.map((item) => [ item.time, item['Flow'] ]); - pumpChart.series[1].data = pumpData.map((item) => [ item.time, item['MotorCurrent'] ]); + pumpChart.series[0].data = pumpData.map((item) => [item.time, item['Flow']]); + pumpChart.series[1].data = pumpData.map((item) => [item.time, item['MotorCurrent']]); pumpChart.series[2].data = pumpData.map((item) => [ item.time, item['StuffingBoxTemperature'], ]); - pumpChart.series[3].data = pumpData.map((item) => [ item.time, item['PressureIn'] ]); - pumpChart.series[4].data = pumpData.map((item) => [ item.time, item['PressureOut'] ]); - + pumpChart.series[3].data = pumpData.map((item) => [item.time, item['PressureIn']]); + pumpChart.series[4].data = pumpData.map((item) => [item.time, item['PressureOut']]); return pumpChart; }); private readonly envOptions: EChartsOption = { @@ -226,62 +217,61 @@ export class XdDetailPage implements OnInit { ], }; - protected readonly envChart: Signal = computed(() => { - const envData = this.envData(); - if (!envData) return undefined; - - const envChart = { - ...this.envOptions, - }; + protected readonly envChart: Signal = computed(() => { + const envData = this.envData(); - if (!envChart.series || !(envChart.series instanceof Array)) return undefined; + if (!envData) return undefined; - envChart.series[0].data = envData.map((item) => [ item.time, item['Temperature'] ]); - envChart.series[1].data = envData.map((item) => [ item.time, item['Humidity'] ]); - envChart.series[2].data = envData.map((item) => [ item.time, item['Pressure'] ]); - return envChart; - }); + const envChart = { + ...this.envOptions, + }; - protected readonly metricsChart: Signal = computed(() => { - const facility = this.facility(); - if (!facility) return undefined; + if (!envChart.series || !(envChart.series instanceof Array)) return undefined; - const metrics = facility.metrics; + envChart.series[0].data = envData.map((item) => [item.time, item['Temperature']]); + envChart.series[1].data = envData.map((item) => [item.time, item['Humidity']]); + envChart.series[2].data = envData.map((item) => [item.time, item['Pressure']]); + return envChart; + }); - if (!metrics || Array.isArray(metrics) && metrics.length === 0) return undefined; + protected readonly metricsChart: Signal = computed(() => { + const metricsData = this.metricsData(); - const xAxisData = map(metrics, item => PUMP_METRICS_FULL_NAME_MAP[item.name].replace(/ /g, '\n').trim()); - const seriesKeys = $enum(EMetricsCategory).getValues(); + if (!metricsData) return undefined; - const seriesData = map(seriesKeys, (key) => { - return { - name: METRIC_CATEGORY_COLOR_INFORMATION[key].abbreviation, - data: map(metrics, (item: IPumpMetrics) => parseFloat(item[key]!.toFixed(2))), - type: 'bar', - emphasis: { focus: 'series' }, - itemStyle: { color: METRIC_CATEGORY_COLOR_INFORMATION[key].color }, - }; - }); + const xAxisData = map(metricsData, (item) => + PUMP_METRICS_FULL_NAME_MAP[item.name].replace(/ /g, '\n').trim(), + ); + const seriesKeys = $enum(EMetricsCategory).getValues(); - return defaults(this.barChartOptions, { - xAxis: { - type: 'category', - data: xAxisData, - nameLocation: 'middle', - axisLabel: { - width: 100, - overflow: 'truncate', - interval: 0, - }, - }, - series: seriesData, - }); - }); + const seriesData = map(seriesKeys, (key) => { + return { + name: METRIC_CATEGORY_COLOR_INFORMATION[key].abbreviation, + data: map(metricsData, (item: IPumpMetrics) => item[key]), + type: 'bar', + emphasis: { focus: 'series' }, + itemStyle: { color: METRIC_CATEGORY_COLOR_INFORMATION[key].color }, + }; + }); + return defaults(this.barChartOptions, { + xAxis: { + type: 'category', + data: xAxisData, + nameLocation: 'middle', + axisLabel: { + width: 100, + overflow: 'truncate', + interval: 0, + }, + }, + series: seriesData, + }); + }); constructor( protected route: ActivatedRoute, - protected location: Location, + protected location: Location, private readonly _modalService: ModalService, ) {} @@ -289,7 +279,7 @@ export class XdDetailPage implements OnInit { registerTheme(echarts); themeSwitcher.themeChanged.on((theme: string) => { - this.theme.set(convertThemeName(theme)); + this.theme.set(convertThemeName(theme)); }); } @@ -306,17 +296,16 @@ export class XdDetailPage implements OnInit { }); } - mapNth(n: number) { - switch (n) { - case 1: - return 'First'; - case 2: - return 'Second'; - case 3: - return `${n}rd` - default: - return `${n}th`; - } - } - + mapNth(n: number) { + switch (n) { + case 1: + return 'First'; + case 2: + return 'Second'; + case 3: + return `${n}rd`; + default: + return `${n}th`; + } + } } From fa68a625107ec0a1f04ab44f33169fe96e2a6c0a Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 15 Jul 2024 09:58:34 +0200 Subject: [PATCH 21/21] refactor: remove comment Co-authored-by: Ingo Sternberg Signed-off-by: Jonas --- .../src/lib/services/timeseries.service.ts | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.ts b/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.ts index 5294297e..d13c3210 100644 --- a/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.ts +++ b/libs/facilities/backend/timeseries/src/lib/services/timeseries.service.ts @@ -145,34 +145,6 @@ export class XdTimeseriesService { ); } - // return from( - // this.prismaService.timeSeriesDataItem.findMany({ - // where: { - // timeSeriesItemAssetId: assetId, - // timeSeriesItemPropertySetName: propertySetName, - // time: { - // gte: args.from, - // lte: args.to, - // }, - // }, - // take: args.limit, - // orderBy: { - // time: args.sort, - // }, - // }), - // ).pipe( - // map((items) => { - // return items.map((item) => ({ - // time: item.time, - // ...this.prismaService.selectKeysFromJSON(item.data, args.select), - // })); - // }), - // catchError((err: Error) => { - // throw err; - // }), - // ); - // } - /** * Get all timeseries data */