Merge pull request #634 from bauzer714/addDeviceHoursSetting

Create a setting for user to indicate humanized or hours on dashboard/device detail
This commit is contained in:
Jason Kulatunga
2024-07-25 13:33:57 -07:00
committed by GitHub
11 changed files with 121 additions and 18 deletions
@@ -332,7 +332,6 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
SettingDataType: "string", SettingDataType: "string",
SettingValueString: "smooth", SettingValueString: "smooth",
}, },
{ {
SettingKeyName: "metrics.notify_level", SettingKeyName: "metrics.notify_level",
SettingKeyDescription: "Determines which device status will cause a notification (fail or warn)", SettingKeyDescription: "Determines which device status will cause a notification (fail or warn)",
@@ -385,6 +384,21 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
return tx.Create(&defaultSettings).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 { if err := m.Migrate(); err != nil {
@@ -421,8 +435,8 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
// helpers // helpers
//When adding data to influxdb, an error may be returned if the data point is outside the range of the retention policy. // 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. // This function will ignore retention policy errors, and allow the migration to continue.
func ignorePastRetentionPolicyError(err error) error { func ignorePastRetentionPolicyError(err error) error {
var influxDbWriteError *http.Error var influxDbWriteError *http.Error
if errors.As(err, &influxDbWriteError) { if errors.As(err, &influxDbWriteError) {
+8 -7
View File
@@ -8,13 +8,14 @@ package models
//} //}
type Settings struct { type Settings struct {
Theme string `json:"theme" mapstructure:"theme"` Theme string `json:"theme" mapstructure:"theme"`
Layout string `json:"layout" mapstructure:"layout"` Layout string `json:"layout" mapstructure:"layout"`
DashboardDisplay string `json:"dashboard_display" mapstructure:"dashboard_display"` DashboardDisplay string `json:"dashboard_display" mapstructure:"dashboard_display"`
DashboardSort string `json:"dashboard_sort" mapstructure:"dashboard_sort"` DashboardSort string `json:"dashboard_sort" mapstructure:"dashboard_sort"`
TemperatureUnit string `json:"temperature_unit" mapstructure:"temperature_unit"` TemperatureUnit string `json:"temperature_unit" mapstructure:"temperature_unit"`
FileSizeSIUnits bool `json:"file_size_si_units" mapstructure:"file_size_si_units"` FileSizeSIUnits bool `json:"file_size_si_units" mapstructure:"file_size_si_units"`
LineStroke string `json:"line_stroke" mapstructure:"line_stroke"` LineStroke string `json:"line_stroke" mapstructure:"line_stroke"`
PoweredOnHoursUnit string `json:"powered_on_hours_unit" mapstructure:"powered_on_hours_unit"`
Metrics struct { Metrics struct {
NotifyLevel int `json:"notify_level" mapstructure:"notify_level"` NotifyLevel int `json:"notify_level" mapstructure:"notify_level"`
@@ -12,6 +12,8 @@ export type TemperatureUnit = 'celsius' | 'fahrenheit'
export type LineStroke = 'smooth' | 'straight' | 'stepline' export type LineStroke = 'smooth' | 'straight' | 'stepline'
export type DevicePoweredOnUnit = 'humanize' | 'device_hours'
export enum MetricsNotifyLevel { export enum MetricsNotifyLevel {
Warn = 1, Warn = 1,
@@ -47,6 +49,8 @@ export interface AppConfig {
file_size_si_units?: boolean; file_size_si_units?: boolean;
powered_on_hours_unit?: DevicePoweredOnUnit;
line_stroke?: LineStroke; line_stroke?: LineStroke;
// Settings from Scrutiny API // Settings from Scrutiny API
@@ -77,6 +81,7 @@ export const appConfig: AppConfig = {
temperature_unit: 'celsius', temperature_unit: 'celsius',
file_size_si_units: false, file_size_si_units: false,
powered_on_hours_unit: 'humanize',
line_stroke: 'smooth', line_stroke: 'smooth',
@@ -63,7 +63,7 @@
</div> </div>
<div class="flex flex-col mx-6 my-3 xs:w-full"> <div class="flex flex-col mx-6 my-3 xs:w-full">
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Powered On</div> <div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Powered On</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.power_on_hours; else unknownPoweredOn">{{ humanizeDuration(deviceSummary.smart?.power_on_hours * 60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] }) }}</div> <div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.power_on_hours; else unknownPoweredOn">{{ deviceSummary.smart?.power_on_hours | deviceHours:config.powered_on_hours_unit:{ round: true, largest: 1, units: ['y', 'd', 'h'] } }}</div>
<ng-template #unknownPoweredOn><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template> <ng-template #unknownPoweredOn><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
</div> </div>
</div> </div>
@@ -4,7 +4,6 @@ import {takeUntil} from 'rxjs/operators';
import {AppConfig} from 'app/core/config/app.config'; import {AppConfig} from 'app/core/config/app.config';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service'; import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
import {Subject} from 'rxjs'; import {Subject} from 'rxjs';
import humanizeDuration from 'humanize-duration'
import {MatDialog} from '@angular/material/dialog'; import {MatDialog} from '@angular/material/dialog';
import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component'; import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component';
import {DeviceTitlePipe} from 'app/shared/device-title.pipe'; import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
@@ -34,8 +33,6 @@ export class DashboardDeviceComponent implements OnInit {
private _unsubscribeAll: Subject<void>; private _unsubscribeAll: Subject<void>;
readonly humanizeDuration = humanizeDuration;
deviceStatusForModelWithThreshold = DeviceStatusPipe.deviceStatusForModelWithThreshold deviceStatusForModelWithThreshold = DeviceStatusPipe.deviceStatusForModelWithThreshold
ngOnInit(): void { ngOnInit(): void {
@@ -54,6 +54,14 @@
</div> </div>
<div class="flex flex-col mt-5 gt-md:flex-row"> <div class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
<mat-label>Powered On Format</mat-label>
<mat-select [(ngModel)]="poweredOnHoursUnit">
<mat-option value="humanize">Humanize</mat-option>
<mat-option value="device_hours">Device Hours</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3"> <mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
<mat-label>Line stroke</mat-label> <mat-label>Line stroke</mat-label>
<mat-select [(ngModel)]="lineStroke"> <mat-select [(ngModel)]="lineStroke">
@@ -7,7 +7,8 @@ import {
MetricsStatusThreshold, MetricsStatusThreshold,
TemperatureUnit, TemperatureUnit,
LineStroke, LineStroke,
Theme Theme,
DevicePoweredOnUnit
} from 'app/core/config/app.config'; } from 'app/core/config/app.config';
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service'; import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
import {Subject} from 'rxjs'; import {Subject} from 'rxjs';
@@ -24,6 +25,7 @@ export class DashboardSettingsComponent implements OnInit {
dashboardSort: string; dashboardSort: string;
temperatureUnit: string; temperatureUnit: string;
fileSizeSIUnits: boolean; fileSizeSIUnits: boolean;
poweredOnHoursUnit: string;
lineStroke: string; lineStroke: string;
theme: string; theme: string;
statusThreshold: number; statusThreshold: number;
@@ -51,6 +53,7 @@ export class DashboardSettingsComponent implements OnInit {
this.dashboardSort = config.dashboard_sort; this.dashboardSort = config.dashboard_sort;
this.temperatureUnit = config.temperature_unit; this.temperatureUnit = config.temperature_unit;
this.fileSizeSIUnits = config.file_size_si_units; this.fileSizeSIUnits = config.file_size_si_units;
this.poweredOnHoursUnit = config.powered_on_hours_unit;
this.lineStroke = config.line_stroke; this.lineStroke = config.line_stroke;
this.theme = config.theme; this.theme = config.theme;
@@ -68,6 +71,7 @@ export class DashboardSettingsComponent implements OnInit {
dashboard_sort: this.dashboardSort as DashboardSort, dashboard_sort: this.dashboardSort as DashboardSort,
temperature_unit: this.temperatureUnit as TemperatureUnit, temperature_unit: this.temperatureUnit as TemperatureUnit,
file_size_si_units: this.fileSizeSIUnits, file_size_si_units: this.fileSizeSIUnits,
powered_on_hours_unit: this.poweredOnHoursUnit as DevicePoweredOnUnit,
line_stroke: this.lineStroke as LineStroke, line_stroke: this.lineStroke as LineStroke,
theme: this.theme as Theme, theme: this.theme as Theme,
metrics: { metrics: {
@@ -123,7 +123,7 @@
<div class="text-secondary text-md">Power Cycle Count</div> <div class="text-secondary text-md">Power Cycle Count</div>
</div> </div>
<div *ngIf="smart_results[0]?.power_on_hours" class="my-2 col-span-2 lt-md:col-span-1"> <div *ngIf="smart_results[0]?.power_on_hours" class="my-2 col-span-2 lt-md:col-span-1">
<div matTooltip="{{humanizeDuration(smart_results[0]?.power_on_hours * 60 * 60 * 1000, { conjunction: ' and ', serialComma: false })}}">{{humanizeDuration(smart_results[0]?.power_on_hours *60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] })}}</div> <div matTooltip="{{humanizeDuration(smart_results[0]?.power_on_hours * 60 * 60 * 1000, { conjunction: ' and ', serialComma: false })}}">{{ smart_results[0]?.power_on_hours | deviceHours:config.powered_on_hours_unit:{ round: true, largest: 1, units: ['y', 'd', 'h'] } }}</div>
<div class="text-secondary text-md">Powered On</div> <div class="text-secondary text-md">Powered On</div>
</div> </div>
<div class="my-2 col-span-2 lt-md:col-span-1"> <div class="my-2 col-span-2 lt-md:col-span-1">
@@ -0,0 +1,52 @@
import { DeviceHoursPipe } from "./device-hours.pipe";
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: "Unknown",
},
{
input: null,
configuration: "humanize",
result: "Unknown",
},
];
testCases.forEach((test, index) => {
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);
expect(formatted).toEqual(test.result);
});
});
});
});
@@ -0,0 +1,19 @@
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 (hoursOfRunTime === null) {
return 'Unknown';
}
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)
}
}
@@ -6,6 +6,7 @@ import { DeviceSortPipe } from './device-sort.pipe';
import { TemperaturePipe } from './temperature.pipe'; import { TemperaturePipe } from './temperature.pipe';
import { DeviceTitlePipe } from './device-title.pipe'; import { DeviceTitlePipe } from './device-title.pipe';
import { DeviceStatusPipe } from './device-status.pipe'; import { DeviceStatusPipe } from './device-status.pipe';
import { DeviceHoursPipe } from './device-hours.pipe';
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -13,7 +14,8 @@ import { DeviceStatusPipe } from './device-status.pipe';
DeviceSortPipe, DeviceSortPipe,
TemperaturePipe, TemperaturePipe,
DeviceTitlePipe, DeviceTitlePipe,
DeviceStatusPipe DeviceStatusPipe,
DeviceHoursPipe
], ],
imports: [ imports: [
CommonModule, CommonModule,
@@ -28,7 +30,8 @@ import { DeviceStatusPipe } from './device-status.pipe';
DeviceSortPipe, DeviceSortPipe,
DeviceTitlePipe, DeviceTitlePipe,
DeviceStatusPipe, DeviceStatusPipe,
TemperaturePipe TemperaturePipe,
DeviceHoursPipe
] ]
}) })
export class SharedModule export class SharedModule