consolidate device status to string logic in DeviceStatusPipe.

Ensure device status takes into account new settings.
This commit is contained in:
Jason Kulatunga
2022-07-29 07:11:57 -07:00
parent 2e768fb491
commit ce2f990eb1
7 changed files with 222 additions and 159 deletions
@@ -1,15 +1,15 @@
<div [ngClass]="{ 'border-green': deviceStatusString(deviceSummary) == 'passed', <div [ngClass]="{ 'border-green': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'passed',
'border-red': deviceStatusString(deviceSummary) == 'failed' }" 'border-red': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'failed' }"
class="relative flex flex-col flex-auto p-6 pr-3 pb-3 bg-card rounded border-l-4 shadow-md overflow-hidden"> class="relative flex flex-col flex-auto p-6 pr-3 pb-3 bg-card rounded border-l-4 shadow-md overflow-hidden">
<div class="absolute bottom-0 right-0 w-24 h-24 -m-6"> <div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
<mat-icon class="icon-size-96 opacity-12 text-green" <mat-icon class="icon-size-96 opacity-12 text-green"
*ngIf="deviceStatusString(deviceSummary) == 'passed'" *ngIf="deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'passed'"
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon> [svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-red" <mat-icon class="icon-size-96 opacity-12 text-red"
*ngIf="deviceStatusString(deviceSummary) == 'failed'" *ngIf="deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'failed'"
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon> [svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-yellow" <mat-icon class="icon-size-96 opacity-12 text-yellow"
*ngIf="deviceStatusString(deviceSummary) == 'unknown'" *ngIf="deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'unknown'"
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon> [svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
@@ -47,7 +47,7 @@
<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">Status</div> <div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Status</div>
<div class="mt-2 font-medium text-3xl leading-none" <div class="mt-2 font-medium text-3xl leading-none"
*ngIf="deviceSummary.smart?.collector_date; else unknownStatus">{{ deviceStatusString(deviceSummary) | titlecase}}</div> *ngIf="deviceSummary.smart?.collector_date; else unknownStatus">{{ deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) | titlecase}}</div>
<ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template> <ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
</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">
@@ -155,115 +155,4 @@ describe('DashboardDeviceComponent', () => {
} as DeviceSummaryModel)).toBe('text-red') } as DeviceSummaryModel)).toBe('text-red')
}); });
}) })
describe('#deviceStatusString()', () => {
it('if healthy device, should be passing', () => {
httpClientSpy.get.and.returnValue(of({
settings: {
metrics: {
status_threshold: MetricsStatusThreshold.Both,
}
}
}));
component.ngOnInit()
expect(component.deviceStatusString({
device: {
device_status: 0
},
smart: {
collector_date: moment().subtract(13, 'days').toISOString()
},
} as DeviceSummaryModel)).toBe('passed')
});
it('if device with no smart data, should be unknown', () => {
httpClientSpy.get.and.returnValue(of({
settings: {
metrics: {
status_threshold: MetricsStatusThreshold.Both,
}
}
}));
component.ngOnInit()
expect(component.deviceStatusString({
device: {
device_status: 0
},
} as DeviceSummaryModel)).toBe('unknown')
});
const testCases = [
{
'deviceStatus': 1,
'threshold': MetricsStatusThreshold.Smart,
'result': 'failed'
},
{
'deviceStatus': 1,
'threshold': MetricsStatusThreshold.Scrutiny,
'result': 'passed'
},
{
'deviceStatus': 1,
'threshold': MetricsStatusThreshold.Both,
'result': 'failed'
},
{
'deviceStatus': 2,
'threshold': MetricsStatusThreshold.Smart,
'result': 'passed'
},
{
'deviceStatus': 2,
'threshold': MetricsStatusThreshold.Scrutiny,
'result': 'failed'
},
{
'deviceStatus': 2,
'threshold': MetricsStatusThreshold.Both,
'result': 'failed'
},
{
'deviceStatus': 3,
'threshold': MetricsStatusThreshold.Smart,
'result': 'failed'
},
{
'deviceStatus': 3,
'threshold': MetricsStatusThreshold.Scrutiny,
'result': 'failed'
},
{
'deviceStatus': 3,
'threshold': MetricsStatusThreshold.Both,
'result': 'failed'
}
]
testCases.forEach((test, index) => {
it(`if device with status (${test.deviceStatus}) and threshold (${test.threshold}), should be ${test.result}`, () => {
httpClientSpy.get.and.returnValue(of({
settings: {
metrics: {
status_threshold: test.threshold,
}
}
}));
component.ngOnInit()
expect(component.deviceStatusString({
device: {
device_status: test.deviceStatus
},
smart: {
collector_date: moment().subtract(13, 'days').toISOString()
},
} as DeviceSummaryModel)).toBe(test.result)
});
});
})
}); });
@@ -9,8 +9,7 @@ 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';
import {DeviceSummaryModel} from 'app/core/models/device-summary-model'; import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
import {DeviceStatusPipe} from 'app/shared/device-status.pipe';
export type deviceStatusName = 'unknown' | 'passed' | 'failed'
@Component({ @Component({
selector: 'app-dashboard-device', selector: 'app-dashboard-device',
@@ -37,6 +36,8 @@ export class DashboardDeviceComponent implements OnInit {
readonly humanizeDuration = humanizeDuration; readonly humanizeDuration = humanizeDuration;
deviceStatusForModelWithThreshold = DeviceStatusPipe.deviceStatusForModelWithThreshold
ngOnInit(): void { ngOnInit(): void {
// Subscribe to config changes // Subscribe to config changes
this._configService.config$ this._configService.config$
@@ -52,7 +53,7 @@ export class DashboardDeviceComponent implements OnInit {
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
classDeviceLastUpdatedOn(deviceSummary: DeviceSummaryModel): string { classDeviceLastUpdatedOn(deviceSummary: DeviceSummaryModel): string {
const deviceStatus = this.deviceStatusString(deviceSummary) const deviceStatus = DeviceStatusPipe.deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, this.config.metrics.status_threshold)
if (deviceStatus === 'failed') { if (deviceStatus === 'failed') {
return 'text-red' // if the device has failed, always highlight in red return 'text-red' // if the device has failed, always highlight in red
} else if (deviceStatus === 'passed') { } else if (deviceStatus === 'passed') {
@@ -71,24 +72,6 @@ export class DashboardDeviceComponent implements OnInit {
} }
} }
deviceStatusString(deviceSummary: DeviceSummaryModel): deviceStatusName {
// no smart data, so treat the device status as unknown
if (!deviceSummary.smart) {
return 'unknown'
}
// determine the device status, by comparing it against the allowed threshold
// tslint:disable-next-line:no-bitwise
const deviceStatus = deviceSummary.device.device_status & this.config.metrics.status_threshold
if (deviceStatus === 0) {
return 'passed'
} else {
return 'failed'
}
}
openDeleteDialog(): void { openDeleteDialog(): void {
const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, { const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, {
// width: '250px', // width: '250px',
@@ -56,12 +56,13 @@
<div *ngIf="device" class="my-2 col-span-2 lt-md:col-span-1"> <div *ngIf="device" class="my-2 col-span-2 lt-md:col-span-1">
<div> <div>
<span class="inline-flex items-center font-bold text-xs px-2 py-2px rounded-full tracking-wide uppercase" <span class="inline-flex items-center font-bold text-xs px-2 py-2px rounded-full tracking-wide uppercase"
[ngClass]="{'red-200': device?.device_status != 0, [ngClass]="{'red-200': deviceStatusForModelWithThreshold(device, !!smart_results, config.metrics.status_threshold) == 'failed',
'green-200': device?.device_status == 0}"> 'green-200': device?.device_status == 0}">
<span class="w-2 h-2 rounded-full mr-2" <span class="w-2 h-2 rounded-full mr-2"
[ngClass]="{'bg-red': device?.device_status != 0, [ngClass]="{'bg-red': device?.device_status != 0,
'bg-green': device?.device_status == 0}"></span> 'bg-green': device?.device_status == 0}"></span>
<span class="pr-2px leading-relaxed whitespace-no-wrap">{{device?.device_status | deviceStatus}}</span> <span
class="pr-2px leading-relaxed whitespace-no-wrap">{{device | deviceStatus:!!smart_results:config.metrics.status_threshold:true}}</span>
</span> </span>
</div> </div>
<div class="text-secondary text-md">Status</div> <div class="text-secondary text-md">Status</div>
@@ -16,6 +16,7 @@ import {DeviceModel} from 'app/core/models/device-model';
import {SmartModel} from 'app/core/models/measurements/smart-model'; import {SmartModel} from 'app/core/models/measurements/smart-model';
import {SmartAttributeModel} from 'app/core/models/measurements/smart-attribute-model'; import {SmartAttributeModel} from 'app/core/models/measurements/smart-attribute-model';
import {AttributeMetadataModel} from 'app/core/models/thresholds/attribute-metadata-model'; import {AttributeMetadataModel} from 'app/core/models/thresholds/attribute-metadata-model';
import {DeviceStatusPipe} from 'app/shared/device-status.pipe';
// from Constants.go - these must match // from Constants.go - these must match
const AttributeStatusPassed = 0 const AttributeStatusPassed = 0
@@ -89,6 +90,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
readonly humanizeDuration = humanizeDuration; readonly humanizeDuration = humanizeDuration;
deviceStatusForModelWithThreshold = DeviceStatusPipe.deviceStatusForModelWithThreshold
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks // @ Lifecycle hooks
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
@@ -1,8 +1,146 @@
import { DeviceStatusPipe } from './device-status.pipe'; import {DeviceStatusPipe} from './device-status.pipe';
import {MetricsStatusThreshold} from '../core/config/app.config';
import {DeviceModel} from '../core/models/device-model';
describe('DeviceStatusPipe', () => { describe('DeviceStatusPipe', () => {
it('create an instance', () => { it('create an instance', () => {
const pipe = new DeviceStatusPipe(); const pipe = new DeviceStatusPipe();
expect(pipe).toBeTruthy(); expect(pipe).toBeTruthy();
}); });
describe('#deviceStatusForModelWithThreshold', () => {
it('if healthy device, should be passing', () => {
expect(DeviceStatusPipe.deviceStatusForModelWithThreshold(
{device_status: 0} as DeviceModel,
true,
MetricsStatusThreshold.Both
)).toBe('passed')
});
it('if device with no smart data, should be unknown', () => {
expect(DeviceStatusPipe.deviceStatusForModelWithThreshold(
{device_status: 0} as DeviceModel,
false,
MetricsStatusThreshold.Both
)).toBe('unknown')
});
const testCases = [
{
'deviceStatus': 10000, // invalid status
'hasSmartResults': false,
'threshold': MetricsStatusThreshold.Smart,
'includeReason': false,
'result': 'unknown'
},
{
'deviceStatus': 1,
'hasSmartResults': true,
'threshold': MetricsStatusThreshold.Smart,
'includeReason': false,
'result': 'failed'
},
{
'deviceStatus': 1,
'hasSmartResults': true,
'threshold': MetricsStatusThreshold.Scrutiny,
'includeReason': false,
'result': 'passed'
},
{
'deviceStatus': 1,
'hasSmartResults': true,
'threshold': MetricsStatusThreshold.Both,
'includeReason': false,
'result': 'failed'
},
{
'deviceStatus': 2,
'hasSmartResults': true,
'threshold': MetricsStatusThreshold.Smart,
'includeReason': false,
'result': 'passed'
},
{
'deviceStatus': 2,
'hasSmartResults': true,
'threshold': MetricsStatusThreshold.Scrutiny,
'includeReason': false,
'result': 'failed'
},
{
'deviceStatus': 2,
'hasSmartResults': true,
'threshold': MetricsStatusThreshold.Both,
'includeReason': false,
'result': 'failed'
},
{
'deviceStatus': 3,
'hasSmartResults': true,
'threshold': MetricsStatusThreshold.Smart,
'includeReason': false,
'result': 'failed'
},
{
'deviceStatus': 3,
'hasSmartResults': true,
'threshold': MetricsStatusThreshold.Scrutiny,
'includeReason': false,
'result': 'failed'
},
{
'deviceStatus': 3,
'hasSmartResults': true,
'threshold': MetricsStatusThreshold.Both,
'includeReason': false,
'result': 'failed'
},
{
'deviceStatus': 3,
'hasSmartResults': false,
'threshold': MetricsStatusThreshold.Smart,
'includeReason': true,
'result': 'unknown'
},
{
'deviceStatus': 3,
'hasSmartResults': true,
'threshold': MetricsStatusThreshold.Smart,
'includeReason': true,
'result': 'failed: smart'
},
{
'deviceStatus': 3,
'hasSmartResults': true,
'threshold': MetricsStatusThreshold.Scrutiny,
'includeReason': true,
'result': 'failed: scrutiny'
},
{
'deviceStatus': 3,
'hasSmartResults': true,
'threshold': MetricsStatusThreshold.Both,
'includeReason': true,
'result': 'failed: both'
}
]
testCases.forEach((test, index) => {
it(`if device with status (${test.deviceStatus}), hasSmartResults(${test.hasSmartResults}) and threshold (${test.threshold}), should be ${test.result}`, () => {
expect(DeviceStatusPipe.deviceStatusForModelWithThreshold(
{device_status: test.deviceStatus} as DeviceModel,
test.hasSmartResults,
test.threshold,
test.includeReason
)).toBe(test.result)
});
});
});
}); });
@@ -1,21 +1,71 @@
import { Pipe, PipeTransform } from '@angular/core'; import {Pipe, PipeTransform} from '@angular/core';
import {MetricsStatusThreshold} from '../core/config/app.config';
import {DeviceModel} from '../core/models/device-model';
const DEVICE_STATUS_NAMES: { [key: number]: string } = {
0: 'passed',
1: 'failed',
2: 'failed',
3: 'failed'
};
const DEVICE_STATUS_NAMES_WITH_REASON: { [key: number]: string } = {
0: 'passed',
1: 'failed: smart',
2: 'failed: scrutiny',
3: 'failed: both'
};
@Pipe({ @Pipe({
name: 'deviceStatus' name: 'deviceStatus'
}) })
export class DeviceStatusPipe implements PipeTransform { export class DeviceStatusPipe implements PipeTransform {
transform(deviceStatusFlag: number): string {
if(deviceStatusFlag === 0){ static deviceStatusForModelWithThreshold(
return 'passed' deviceModel: DeviceModel,
} else if(deviceStatusFlag === 3){ hasSmartResults: boolean = true,
return 'failed: both' threshold: MetricsStatusThreshold = MetricsStatusThreshold.Both,
} else if(deviceStatusFlag === 2) { includeReason: boolean = false
return 'failed: scrutiny' ): string {
} else if(deviceStatusFlag === 1) { // no smart data, so treat the device status as unknown
return 'failed: smart' if (!hasSmartResults) {
} return 'unknown'
return 'unknown' }
}
let statusNameLookup = DEVICE_STATUS_NAMES
if (includeReason) {
statusNameLookup = DEVICE_STATUS_NAMES_WITH_REASON
}
// determine the device status, by comparing it against the allowed threshold
// tslint:disable-next-line:no-bitwise
const deviceStatus = deviceModel.device_status & threshold
return statusNameLookup[deviceStatus]
}
// static deviceStatusForModelWithThreshold(deviceModel: DeviceModel | any, threshold: MetricsStatusThreshold): string {
// // tslint:disable-next-line:no-bitwise
// const deviceStatus = deviceModel?.device_status & threshold
// if(deviceStatus === 0){
// return 'passed'
// } else if(deviceStatus === 3){
// return 'failed: both'
// } else if(deviceStatus === 2) {
// return 'failed: scrutiny'
// } else if(deviceStatus === 1) {
// return 'failed: smart'
// }
// return 'unknown'
// }
transform(
deviceModel: DeviceModel,
hasSmartResults: boolean = true,
threshold: MetricsStatusThreshold = MetricsStatusThreshold.Both,
includeReason: boolean = false
): string {
return DeviceStatusPipe.deviceStatusForModelWithThreshold(deviceModel, hasSmartResults, threshold, includeReason)
}
} }