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:
@@ -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,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',
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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 {
|
||||||
|
|||||||
+8
@@ -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">
|
||||||
|
|||||||
+5
-1
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user