From 8fa32c6dd7f6b2fee7ea64b922268dfbeca99e0e Mon Sep 17 00:00:00 2001 From: Brice Bauer Date: Tue, 7 May 2024 16:45:09 -0400 Subject: [PATCH 1/6] Add DB Migration and config/settings --- .../database/scrutiny_repository_migrations.go | 6 ++++++ webapp/backend/pkg/models/settings.go | 15 ++++++++------- webapp/frontend/src/app/core/config/app.config.ts | 5 +++++ .../dashboard-settings.component.html | 8 ++++++++ .../dashboard-settings.component.ts | 6 +++++- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/webapp/backend/pkg/database/scrutiny_repository_migrations.go b/webapp/backend/pkg/database/scrutiny_repository_migrations.go index e1b948b..829c833 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_migrations.go +++ b/webapp/backend/pkg/database/scrutiny_repository_migrations.go @@ -332,6 +332,12 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error { SettingDataType: "string", SettingValueString: "smooth", }, + { + SettingKeyName: "powered_on_hours_unit", + SettingKeyDescription: "Presentation format for device powered on time ('humanize' | 'device-hours')", + SettingDataType: "string", + SettingValueString: "humanize", + }, { SettingKeyName: "metrics.notify_level", diff --git a/webapp/backend/pkg/models/settings.go b/webapp/backend/pkg/models/settings.go index e564301..d40e3e9 100644 --- a/webapp/backend/pkg/models/settings.go +++ b/webapp/backend/pkg/models/settings.go @@ -8,13 +8,14 @@ package models //} type Settings struct { - Theme string `json:"theme" mapstructure:"theme"` - Layout string `json:"layout" mapstructure:"layout"` - DashboardDisplay string `json:"dashboard_display" mapstructure:"dashboard_display"` - DashboardSort string `json:"dashboard_sort" mapstructure:"dashboard_sort"` - TemperatureUnit string `json:"temperature_unit" mapstructure:"temperature_unit"` - FileSizeSIUnits bool `json:"file_size_si_units" mapstructure:"file_size_si_units"` - LineStroke string `json:"line_stroke" mapstructure:"line_stroke"` + Theme string `json:"theme" mapstructure:"theme"` + Layout string `json:"layout" mapstructure:"layout"` + DashboardDisplay string `json:"dashboard_display" mapstructure:"dashboard_display"` + DashboardSort string `json:"dashboard_sort" mapstructure:"dashboard_sort"` + TemperatureUnit string `json:"temperature_unit" mapstructure:"temperature_unit"` + FileSizeSIUnits bool `json:"file_size_si_units" mapstructure:"file_size_si_units"` + LineStroke string `json:"line_stroke" mapstructure:"line_stroke"` + PoweredOnHoursUnit string `json:"powered_on_hours_unit" mapstructure:"powered_on_hours_unit"` Metrics struct { NotifyLevel int `json:"notify_level" mapstructure:"notify_level"` diff --git a/webapp/frontend/src/app/core/config/app.config.ts b/webapp/frontend/src/app/core/config/app.config.ts index 1b6dfe3..c5a1f6f 100644 --- a/webapp/frontend/src/app/core/config/app.config.ts +++ b/webapp/frontend/src/app/core/config/app.config.ts @@ -12,6 +12,8 @@ export type TemperatureUnit = 'celsius' | 'fahrenheit' export type LineStroke = 'smooth' | 'straight' | 'stepline' +export type DevicePoweredOnUnit = 'humanize' | 'device_hours' + export enum MetricsNotifyLevel { Warn = 1, @@ -47,6 +49,8 @@ export interface AppConfig { file_size_si_units?: boolean; + powered_on_hours_unit?: DevicePoweredOnUnit; + line_stroke?: LineStroke; // Settings from Scrutiny API @@ -77,6 +81,7 @@ export const appConfig: AppConfig = { temperature_unit: 'celsius', file_size_si_units: false, + powered_on_hours_unit: 'humanize', line_stroke: 'smooth', diff --git a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html index 0eb9a03..970044c 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html +++ b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html @@ -54,6 +54,14 @@
+ + Powered On Format + + Humanize + Device Hours + + + Line stroke diff --git a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts index 94f262f..ec65157 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts @@ -7,7 +7,8 @@ import { MetricsStatusThreshold, TemperatureUnit, LineStroke, - Theme + Theme, + DevicePoweredOnUnit } from 'app/core/config/app.config'; import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service'; import {Subject} from 'rxjs'; @@ -24,6 +25,7 @@ export class DashboardSettingsComponent implements OnInit { dashboardSort: string; temperatureUnit: string; fileSizeSIUnits: boolean; + poweredOnHoursUnit: string; lineStroke: string; theme: string; statusThreshold: number; @@ -51,6 +53,7 @@ export class DashboardSettingsComponent implements OnInit { this.dashboardSort = config.dashboard_sort; this.temperatureUnit = config.temperature_unit; this.fileSizeSIUnits = config.file_size_si_units; + this.poweredOnHoursUnit = config.powered_on_hours_unit; this.lineStroke = config.line_stroke; this.theme = config.theme; @@ -68,6 +71,7 @@ export class DashboardSettingsComponent implements OnInit { dashboard_sort: this.dashboardSort as DashboardSort, temperature_unit: this.temperatureUnit as TemperatureUnit, file_size_si_units: this.fileSizeSIUnits, + powered_on_hours_unit: this.poweredOnHoursUnit as DevicePoweredOnUnit, line_stroke: this.lineStroke as LineStroke, theme: this.theme as Theme, metrics: { From 806f7c64a0a7626fd65560f2862b0a449538cab2 Mon Sep 17 00:00:00 2001 From: Brice Bauer Date: Wed, 8 May 2024 08:26:20 -0400 Subject: [PATCH 2/6] Add pipe and implement to dashboard/device component --- .../dashboard-device.component.html | 2 +- .../dashboard-device.component.ts | 3 --- .../src/app/modules/detail/detail.component.html | 2 +- .../src/app/shared/device-hours.pipe.spec.ts | 9 +++++++++ .../frontend/src/app/shared/device-hours.pipe.ts | 16 ++++++++++++++++ webapp/frontend/src/app/shared/shared.module.ts | 7 +++++-- 6 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 webapp/frontend/src/app/shared/device-hours.pipe.spec.ts create mode 100644 webapp/frontend/src/app/shared/device-hours.pipe.ts diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html index 43e8964..43f4e0b 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html +++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html @@ -63,7 +63,7 @@
Powered On
-
{{ humanizeDuration(deviceSummary.smart?.power_on_hours * 60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] }) }}
+
{{ deviceSummary.smart?.power_on_hours | deviceHours:config.powered_on_hours_unit:{ round: true, largest: 1, units: ['y', 'd', 'h'] } }}
--
diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts index 57da104..c54bbc7 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts @@ -4,7 +4,6 @@ import {takeUntil} from 'rxjs/operators'; import {AppConfig} from 'app/core/config/app.config'; import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service'; import {Subject} from 'rxjs'; -import humanizeDuration from 'humanize-duration' import {MatDialog} from '@angular/material/dialog'; import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component'; import {DeviceTitlePipe} from 'app/shared/device-title.pipe'; @@ -34,8 +33,6 @@ export class DashboardDeviceComponent implements OnInit { private _unsubscribeAll: Subject; - readonly humanizeDuration = humanizeDuration; - deviceStatusForModelWithThreshold = DeviceStatusPipe.deviceStatusForModelWithThreshold ngOnInit(): void { diff --git a/webapp/frontend/src/app/modules/detail/detail.component.html b/webapp/frontend/src/app/modules/detail/detail.component.html index 0f37778..13b19c2 100644 --- a/webapp/frontend/src/app/modules/detail/detail.component.html +++ b/webapp/frontend/src/app/modules/detail/detail.component.html @@ -123,7 +123,7 @@
Power Cycle Count
-
{{humanizeDuration(smart_results[0]?.power_on_hours *60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] })}}
+
{{ smart_results[0]?.power_on_hours | deviceHours:config.powered_on_hours_unit:{ round: true, largest: 1, units: ['y', 'd', 'h'] } }}
Powered On
diff --git a/webapp/frontend/src/app/shared/device-hours.pipe.spec.ts b/webapp/frontend/src/app/shared/device-hours.pipe.spec.ts new file mode 100644 index 0000000..cd9f1bc --- /dev/null +++ b/webapp/frontend/src/app/shared/device-hours.pipe.spec.ts @@ -0,0 +1,9 @@ +import { DeviceHoursPipe } from './device-hours.pipe'; + + +describe('DeviceHoursPipe', () => { + it('create an instance', () => { + const pipe = new DeviceHoursPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/webapp/frontend/src/app/shared/device-hours.pipe.ts b/webapp/frontend/src/app/shared/device-hours.pipe.ts new file mode 100644 index 0000000..f6b5e1f --- /dev/null +++ b/webapp/frontend/src/app/shared/device-hours.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import humanizeDuration from 'humanize-duration'; + +@Pipe({ name: 'deviceHours' }) +export class DeviceHoursPipe implements PipeTransform { + static format(hoursOfRunTime: number, unit: string, humanizeConfig: object): string { + if (unit === 'device_hours') { + return `${hoursOfRunTime} hours`; + } + return humanizeDuration(hoursOfRunTime * 60 * 60 * 1000, humanizeConfig); + } + + transform(hoursOfRunTime: number, unit = 'humanize', humanizeConfig: any = {}): string { + return DeviceHoursPipe.format(hoursOfRunTime, unit, humanizeConfig) + } +} diff --git a/webapp/frontend/src/app/shared/shared.module.ts b/webapp/frontend/src/app/shared/shared.module.ts index c22fdb2..1f9779b 100644 --- a/webapp/frontend/src/app/shared/shared.module.ts +++ b/webapp/frontend/src/app/shared/shared.module.ts @@ -6,6 +6,7 @@ import { DeviceSortPipe } from './device-sort.pipe'; import { TemperaturePipe } from './temperature.pipe'; import { DeviceTitlePipe } from './device-title.pipe'; import { DeviceStatusPipe } from './device-status.pipe'; +import { DeviceHoursPipe } from './device-hours.pipe'; @NgModule({ declarations: [ @@ -13,7 +14,8 @@ import { DeviceStatusPipe } from './device-status.pipe'; DeviceSortPipe, TemperaturePipe, DeviceTitlePipe, - DeviceStatusPipe + DeviceStatusPipe, + DeviceHoursPipe ], imports: [ CommonModule, @@ -28,7 +30,8 @@ import { DeviceStatusPipe } from './device-status.pipe'; DeviceSortPipe, DeviceTitlePipe, DeviceStatusPipe, - TemperaturePipe + TemperaturePipe, + DeviceHoursPipe ] }) export class SharedModule From a18e2842ac8390cfbb064633b6bbd22ec8b45de6 Mon Sep 17 00:00:00 2001 From: Brice Bauer Date: Wed, 8 May 2024 08:43:25 -0400 Subject: [PATCH 3/6] Update db migration description to match setting possible values --- webapp/backend/pkg/database/scrutiny_repository_migrations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/backend/pkg/database/scrutiny_repository_migrations.go b/webapp/backend/pkg/database/scrutiny_repository_migrations.go index 829c833..e083712 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_migrations.go +++ b/webapp/backend/pkg/database/scrutiny_repository_migrations.go @@ -334,7 +334,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error { }, { SettingKeyName: "powered_on_hours_unit", - SettingKeyDescription: "Presentation format for device powered on time ('humanize' | 'device-hours')", + SettingKeyDescription: "Presentation format for device powered on time ('humanize' | 'device_hours')", SettingDataType: "string", SettingValueString: "humanize", }, From 142fe06df16f5fd138ea3ef012cb9098bdb1cb64 Mon Sep 17 00:00:00 2001 From: Brice Bauer Date: Mon, 22 Jul 2024 08:37:35 -0400 Subject: [PATCH 4/6] Move powered_on_hours_unit to a new migration id --- .../scrutiny_repository_migrations.go | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/webapp/backend/pkg/database/scrutiny_repository_migrations.go b/webapp/backend/pkg/database/scrutiny_repository_migrations.go index e083712..9a02574 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_migrations.go +++ b/webapp/backend/pkg/database/scrutiny_repository_migrations.go @@ -332,13 +332,6 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error { SettingDataType: "string", SettingValueString: "smooth", }, - { - SettingKeyName: "powered_on_hours_unit", - SettingKeyDescription: "Presentation format for device powered on time ('humanize' | 'device_hours')", - SettingDataType: "string", - SettingValueString: "humanize", - }, - { SettingKeyName: "metrics.notify_level", SettingKeyDescription: "Determines which device status will cause a notification (fail or warn)", @@ -391,6 +384,21 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error { return tx.Create(&defaultSettings).Error }, }, + { + ID: "m20240722082740", // add powered_on_hours_unit setting. + Migrate: func(tx *gorm.DB) error { + //add powered_on_hours_unit setting default. + var defaultSettings = []m20220716214900.Setting{ + { + SettingKeyName: "powered_on_hours_unit", + SettingKeyDescription: "Presentation format for device powered on time ('humanize' | 'device_hours')", + SettingDataType: "string", + SettingValueString: "humanize", + }, + } + return tx.Create(&defaultSettings).Error + }, + }, }) if err := m.Migrate(); err != nil { @@ -427,8 +435,8 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error { // helpers -//When adding data to influxdb, an error may be returned if the data point is outside the range of the retention policy. -//This function will ignore retention policy errors, and allow the migration to continue. +// When adding data to influxdb, an error may be returned if the data point is outside the range of the retention policy. +// This function will ignore retention policy errors, and allow the migration to continue. func ignorePastRetentionPolicyError(err error) error { var influxDbWriteError *http.Error if errors.As(err, &influxDbWriteError) { From a5893f0bf9fdff47ee7202e7824b7b64629dffea Mon Sep 17 00:00:00 2001 From: Brice Bauer Date: Mon, 22 Jul 2024 14:02:27 -0400 Subject: [PATCH 5/6] Add tests for DeviceHoursPipe --- .../src/app/shared/device-hours.pipe.spec.ts | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/webapp/frontend/src/app/shared/device-hours.pipe.spec.ts b/webapp/frontend/src/app/shared/device-hours.pipe.spec.ts index cd9f1bc..86b3aa6 100644 --- a/webapp/frontend/src/app/shared/device-hours.pipe.spec.ts +++ b/webapp/frontend/src/app/shared/device-hours.pipe.spec.ts @@ -1,9 +1,54 @@ -import { DeviceHoursPipe } from './device-hours.pipe'; +import { DeviceHoursPipe } from "./device-hours.pipe"; - -describe('DeviceHoursPipe', () => { - it('create an instance', () => { +describe("DeviceHoursPipe", () => { + it("create an instance", () => { const pipe = new DeviceHoursPipe(); expect(pipe).toBeTruthy(); }); + + describe("#transform", () => { + const testCases = [ + { + input: 12345, + configuration: "device_hours", + result: "12345 hours", + }, + { + input: 15273, + configuration: "humanize", + result: "1 year, 8 months, 3 weeks, 6 days, 15 hours", + }, + { + input: 48, + configuration: null, + result: "2 days", + }, + { + input: 168, + configuration: "scrutiny", + result: "1 week", + }, + { + input: null, + configuration: "device_hours", + result: "null hours", + }, + { + input: null, + configuration: "humanize", + result: "0 seconds", + }, + ]; + + testCases.forEach((test, index) => { + it(`should format input ${test.input} with configuration '${ + test.configuration + }' (testcase: ${index + 1})`, () => { + // test + const pipe = new DeviceHoursPipe(); + const formatted = pipe.transform(test.input, test.configuration); + expect(formatted).toEqual(test.result); + }); + }); + }); }); From c5943a1ca4d07789235797dc1d0b59b03e10ab38 Mon Sep 17 00:00:00 2001 From: Brice Bauer Date: Thu, 25 Jul 2024 15:40:28 -0400 Subject: [PATCH 6/6] Adjust null input response, and tests --- webapp/frontend/src/app/shared/device-hours.pipe.spec.ts | 8 +++----- webapp/frontend/src/app/shared/device-hours.pipe.ts | 5 ++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/webapp/frontend/src/app/shared/device-hours.pipe.spec.ts b/webapp/frontend/src/app/shared/device-hours.pipe.spec.ts index 86b3aa6..cabad90 100644 --- a/webapp/frontend/src/app/shared/device-hours.pipe.spec.ts +++ b/webapp/frontend/src/app/shared/device-hours.pipe.spec.ts @@ -31,19 +31,17 @@ describe("DeviceHoursPipe", () => { { input: null, configuration: "device_hours", - result: "null hours", + result: "Unknown", }, { input: null, configuration: "humanize", - result: "0 seconds", + result: "Unknown", }, ]; testCases.forEach((test, index) => { - it(`should format input ${test.input} with configuration '${ - test.configuration - }' (testcase: ${index + 1})`, () => { + it(`format input '${test.input}' with configuration '${test.configuration}', should be '${test.result}' (testcase: ${index + 1})`, () => { // test const pipe = new DeviceHoursPipe(); const formatted = pipe.transform(test.input, test.configuration); diff --git a/webapp/frontend/src/app/shared/device-hours.pipe.ts b/webapp/frontend/src/app/shared/device-hours.pipe.ts index f6b5e1f..1170c8a 100644 --- a/webapp/frontend/src/app/shared/device-hours.pipe.ts +++ b/webapp/frontend/src/app/shared/device-hours.pipe.ts @@ -4,7 +4,10 @@ import humanizeDuration from 'humanize-duration'; @Pipe({ name: 'deviceHours' }) export class DeviceHoursPipe implements PipeTransform { static format(hoursOfRunTime: number, unit: string, humanizeConfig: object): string { - if (unit === 'device_hours') { + if (hoursOfRunTime === null) { + return 'Unknown'; + } + if (unit === 'device_hours') { return `${hoursOfRunTime} hours`; } return humanizeDuration(hoursOfRunTime * 60 * 60 * 1000, humanizeConfig);