Merge pull request #262 from AnalogJ/beta

pre-v0.4.7 release
This commit is contained in:
Jason Kulatunga
2022-05-25 07:50:21 -07:00
committed by GitHub
24 changed files with 653 additions and 165 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ func DevicePrefix() string {
func (d *Detect) Start() ([]models.Device, error) { func (d *Detect) Start() ([]models.Device, error) {
d.Shell = shell.Create() d.Shell = shell.Create()
// call the base/common functionality to get a list of devicess // call the base/common functionality to get a list of devices
detectedDevices, err := d.SmartctlScan() detectedDevices, err := d.SmartctlScan()
if err != nil { if err != nil {
return nil, err return nil, err
+52
View File
@@ -1,9 +1,12 @@
package detect package detect
import ( import (
"fmt"
"github.com/analogj/scrutiny/collector/pkg/common/shell" "github.com/analogj/scrutiny/collector/pkg/common/shell"
"github.com/analogj/scrutiny/collector/pkg/models" "github.com/analogj/scrutiny/collector/pkg/models"
"github.com/jaypipes/ghw" "github.com/jaypipes/ghw"
"io/ioutil"
"path/filepath"
"strings" "strings"
) )
@@ -22,6 +25,7 @@ func (d *Detect) Start() ([]models.Device, error) {
//inflate device info for detected devices. //inflate device info for detected devices.
for ndx, _ := range detectedDevices { for ndx, _ := range detectedDevices {
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors. d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
populateUdevInfo(&detectedDevices[ndx]) //ignore errors.
} }
return detectedDevices, nil return detectedDevices, nil
@@ -49,3 +53,51 @@ func (d *Detect) wwnFallback(detectedDevice *models.Device) {
//wwn must always be lowercase. //wwn must always be lowercase.
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN) detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
} }
// as discussed in
// - https://github.com/AnalogJ/scrutiny/issues/225
// - https://github.com/jaypipes/ghw/issues/59#issue-361915216
// udev exposes its data in a standardized way under /run/udev/data/....
func populateUdevInfo(detectedDevice *models.Device) error {
// Get device major:minor numbers
// `cat /sys/class/block/sda/dev`
devNo, err := ioutil.ReadFile(filepath.Join("/sys/class/block/", detectedDevice.DeviceName, "dev"))
if err != nil {
return err
}
// Look up block device in udev runtime database
// `cat /run/udev/data/b8:0`
udevID := "b" + strings.TrimSpace(string(devNo))
udevBytes, err := ioutil.ReadFile(filepath.Join("/run/udev/data/", udevID))
if err != nil {
return err
}
deviceMountPaths := []string{}
udevInfo := make(map[string]string)
for _, udevLine := range strings.Split(string(udevBytes), "\n") {
if strings.HasPrefix(udevLine, "E:") {
if s := strings.SplitN(udevLine[2:], "=", 2); len(s) == 2 {
udevInfo[s[0]] = s[1]
}
} else if strings.HasPrefix(udevLine, "S:") {
deviceMountPaths = append(deviceMountPaths, udevLine[2:])
}
}
//Set additional device information.
if deviceLabel, exists := udevInfo["ID_FS_LABEL"]; exists {
detectedDevice.DeviceLabel = deviceLabel
}
if deviceUUID, exists := udevInfo["ID_FS_UUID"]; exists {
detectedDevice.DeviceUUID = deviceUUID
}
if deviceSerialID, exists := udevInfo["ID_SERIAL"]; exists {
detectedDevice.DeviceSerialID = fmt.Sprintf("%s-%s", udevInfo["ID_BUS"], deviceSerialID)
}
return nil
}
+9 -2
View File
@@ -1,10 +1,13 @@
package models package models
type Device struct { type Device struct {
WWN string `json:"wwn"` WWN string `json:"wwn"`
HostId string `json:"host_id"`
DeviceName string `json:"device_name"` DeviceName string `json:"device_name"`
DeviceUUID string `json:"device_uuid"`
DeviceSerialID string `json:"device_serial_id"`
DeviceLabel string `json:"device_label"`
Manufacturer string `json:"manufacturer"` Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"` ModelName string `json:"model_name"`
InterfaceType string `json:"interface_type"` InterfaceType string `json:"interface_type"`
@@ -17,6 +20,10 @@ type Device struct {
SmartSupport bool `json:"smart_support"` SmartSupport bool `json:"smart_support"`
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI) DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector. DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.
// User provided metadata
Label string `json:"label"`
HostId string `json:"host_id"`
} }
type DeviceWrapper struct { type DeviceWrapper struct {
@@ -5,6 +5,7 @@ import (
"time" "time"
) )
// Deprecated: m20220503120000.Device is deprecated, only used by db migrations
type Device struct { type Device struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html //GORM attributes, see: http://gorm.io/docs/conventions.html
CreatedAt time.Time CreatedAt time.Time
@@ -0,0 +1,41 @@
package m20220509170100
import (
"github.com/analogj/scrutiny/webapp/backend/pkg"
"time"
)
type Device struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
WWN string `json:"wwn" gorm:"primary_key"`
DeviceName string `json:"device_name"`
DeviceUUID string `json:"device_uuid"`
DeviceSerialID string `json:"device_serial_id"`
DeviceLabel string `json:"device_label"`
Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"`
InterfaceType string `json:"interface_type"`
InterfaceSpeed string `json:"interface_speed"`
SerialNumber string `json:"serial_number"`
Firmware string `json:"firmware"`
RotationSpeed int `json:"rotational_speed"`
Capacity int64 `json:"capacity"`
FormFactor string `json:"form_factor"`
SmartSupport bool `json:"smart_support"`
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.
// User provided metadata
Label string `json:"label"`
HostId string `json:"host_id"`
// Data set by Scrutiny
DeviceStatus pkg.DeviceStatus `json:"device_status"`
}
@@ -18,7 +18,7 @@ import (
func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev models.Device) error { func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev models.Device) error {
if err := sr.gormClient.WithContext(ctx).Clauses(clause.OnConflict{ if err := sr.gormClient.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "wwn"}}, Columns: []clause.Column{{Name: "wwn"}},
DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type"}), DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type", "device_uuid", "device_serial_id", "device_label"}),
}).Create(&dev).Error; err != nil { }).Create(&dev).Error; err != nil {
return err return err
} }
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20201107210306" "github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20201107210306"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000" "github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000"
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220509170100"
"github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/analogj/scrutiny/webapp/backend/pkg/models"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector" "github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements" "github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
@@ -253,10 +254,19 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
return err return err
} }
//migrate the device database to the current version //migrate the device database
return tx.AutoMigrate(m20220503120000.Device{}) return tx.AutoMigrate(m20220503120000.Device{})
}, },
}, },
{
ID: "m20220509170100", // addl udev device data
Migrate: func(tx *gorm.DB) error {
//migrate the device database.
// adding addl columns (device_label, device_uuid, device_serial_id)
return tx.AutoMigrate(m20220509170100.Device{})
},
},
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
+4
View File
@@ -21,6 +21,10 @@ type Device struct {
WWN string `json:"wwn" gorm:"primary_key"` WWN string `json:"wwn" gorm:"primary_key"`
DeviceName string `json:"device_name"` DeviceName string `json:"device_name"`
DeviceUUID string `json:"device_uuid"`
DeviceSerialID string `json:"device_serial_id"`
DeviceLabel string `json:"device_label"`
Manufacturer string `json:"manufacturer"` Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"` ModelName string `json:"model_name"`
InterfaceType string `json:"interface_type"` InterfaceType string `json:"interface_type"`
@@ -3,6 +3,8 @@ import { BehaviorSubject, Observable } from 'rxjs';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { TREO_APP_CONFIG } from '@treo/services/config/config.constants'; import { TREO_APP_CONFIG } from '@treo/services/config/config.constants';
const SCRUTINY_CONFIG_LOCAL_STORAGE_KEY = 'scrutiny';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
@@ -10,14 +12,22 @@ export class TreoConfigService
{ {
// Private // Private
private _config: BehaviorSubject<any>; private _config: BehaviorSubject<any>;
/** /**
* Constructor * Constructor
*/ */
constructor(@Inject(TREO_APP_CONFIG) config: any) constructor(@Inject(TREO_APP_CONFIG) defaultConfig: any)
{ {
let currentScrutinyConfig = defaultConfig
let localConfigStr = localStorage.getItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY)
if(localConfigStr){
//check localstorage for a value
let localConfig = JSON.parse(localConfigStr)
currentScrutinyConfig = localConfig
}
// Set the private defaults // Set the private defaults
this._config = new BehaviorSubject(config); this._config = new BehaviorSubject(currentScrutinyConfig);
} }
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
@@ -27,15 +37,20 @@ export class TreoConfigService
/** /**
* Setter and getter for config * Setter and getter for config
*/ */
//Setter
set config(value: any) set config(value: any)
{ {
// Merge the new config over to the current config // Merge the new config over to the current config
const config = _.merge({}, this._config.getValue(), value); const config = _.merge({}, this._config.getValue(), value);
//Store the config in localstorage
localStorage.setItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
// Execute the observable // Execute the observable
this._config.next(config); this._config.next(config);
} }
//Getter
get config$(): Observable<any> get config$(): Observable<any>
{ {
return this._config.asObservable(); return this._config.asObservable();
@@ -11,6 +11,11 @@ export interface AppConfig
{ {
theme: Theme; theme: Theme;
layout: Layout; layout: Layout;
// Dashboard options
dashboardDisplay: string;
dashboardSort: string;
} }
/** /**
@@ -23,6 +28,9 @@ export interface AppConfig
*/ */
export const appConfig: AppConfig = { export const appConfig: AppConfig = {
theme : "light", theme : "light",
layout: "material" layout: "material",
dashboardDisplay: "name",
dashboardSort: "status",
}; };
@@ -11,6 +11,9 @@ export const summary = {
"DeletedAt": null, "DeletedAt": null,
"wwn": "0x5000c500673e6b5f", "wwn": "0x5000c500673e6b5f",
"device_name": "sdg", "device_name": "sdg",
"device_label": "14TB-WD-DRIVE2",
"device_uuid": "",
"device_serial_id": "ata-ST6000DX000-1H217Z-Z4DXXXXX",
"manufacturer": "ATA", "manufacturer": "ATA",
"model_name": "ST6000DX000-1H217Z", "model_name": "ST6000DX000-1H217Z",
"interface_type": "SCSI", "interface_type": "SCSI",
@@ -35,6 +38,9 @@ export const summary = {
"DeletedAt": null, "DeletedAt": null,
"wwn": "0x5000cca252c859cc", "wwn": "0x5000cca252c859cc",
"device_name": "sdd", "device_name": "sdd",
"device_label": "14TB-WD-DRIVE1",
"device_uuid": "806cf4bc-d160-4d96-8ee9-3ab7cf2a2e1f",
"device_serial_id": "ata-WDC_WD80EFAX-68LHPN0-7SGLXXXXX",
"manufacturer": "ATA", "manufacturer": "ATA",
"model_name": "WDC_WD80EFAX-68LHPN0", "model_name": "WDC_WD80EFAX-68LHPN0",
"interface_type": "SCSI", "interface_type": "SCSI",
@@ -68,6 +74,9 @@ export const summary = {
"DeletedAt": null, "DeletedAt": null,
"wwn": "0x5000cca264eb01d7", "wwn": "0x5000cca264eb01d7",
"device_name": "sdb", "device_name": "sdb",
"device_label": "14TB-WD-DRIVE5",
"device_uuid": "8125ec6d-a7e4-4950-ac84-72d6a4d67128",
"device_serial_id": "ata-WDC_WD140EDFZ-11A0VA0-9RK1XXXXX",
"manufacturer": "ATA", "manufacturer": "ATA",
"model_name": "WDC_WD140EDFZ-11A0VA0", "model_name": "WDC_WD140EDFZ-11A0VA0",
"interface_type": "SCSI", "interface_type": "SCSI",
@@ -101,6 +110,9 @@ export const summary = {
"DeletedAt": null, "DeletedAt": null,
"wwn": "0x5000cca264ebc248", "wwn": "0x5000cca264ebc248",
"device_name": "sde", "device_name": "sde",
"device_label": "14TB-WD-DRIVE3",
"device_uuid": "9eb60cde-d6d0-4172-b520-b241a6a5477f",
"device_serial_id": "ata-WDC_WD140EDFZ-11A0VA0-9RK3XXXXX",
"manufacturer": "ATA", "manufacturer": "ATA",
"model_name": "WDC_WD140EDFZ-11A0VA0", "model_name": "WDC_WD140EDFZ-11A0VA0",
"interface_type": "SCSI", "interface_type": "SCSI",
@@ -125,6 +137,9 @@ export const summary = {
"DeletedAt": null, "DeletedAt": null,
"wwn": "0x5000cca264ec3183", "wwn": "0x5000cca264ec3183",
"device_name": "sdc", "device_name": "sdc",
"device_label": "14TB-WD-DRIVE6",
"device_uuid": "e1378723-7861-49b9-8e01-0bd063f0ecdd",
"device_serial_id": "ata-WDC_WD140EDFZ-11A0VA0-9RK4XXXXX",
"manufacturer": "ATA", "manufacturer": "ATA",
"model_name": "WDC_WD140EDFZ-11A0VA0", "model_name": "WDC_WD140EDFZ-11A0VA0",
"interface_type": "SCSI", "interface_type": "SCSI",
@@ -138,7 +153,7 @@ export const summary = {
"device_protocol": "", "device_protocol": "",
"device_type": "", "device_type": "",
"label": "", "label": "",
"host_id": "", "host_id": "custom host id",
"device_status": 1 "device_status": 1
}, },
"smart": { "smart": {
@@ -542,6 +557,9 @@ export const summary = {
"DeletedAt": null, "DeletedAt": null,
"wwn": "0x50014ee20b2a72a9", "wwn": "0x50014ee20b2a72a9",
"device_name": "sdf", "device_name": "sdf",
"device_label": "8.0TB-WD-4",
"device_uuid": "fc684dcc-aa2f-44f3-a958-d302dc7dd46d",
"device_serial_id": "ata-WDC_WD60EFRX-68MYMN1-WXL1HXXXXX",
"manufacturer": "ATA", "manufacturer": "ATA",
"model_name": "WDC_WD60EFRX-68MYMN1", "model_name": "WDC_WD60EFRX-68MYMN1",
"interface_type": "SCSI", "interface_type": "SCSI",
@@ -566,6 +584,9 @@ export const summary = {
"DeletedAt": null, "DeletedAt": null,
"wwn": "0x5002538e40a22954", "wwn": "0x5002538e40a22954",
"device_name": "sda", "device_name": "sda",
"device_label": "",
"device_uuid": "",
"device_serial_id": "ata-Samsung_SSD_860_EVO_500GB-S3YZNB0KBXXXXXX",
"manufacturer": "ATA", "manufacturer": "ATA",
"model_name": "Samsung_SSD_860_EVO_500GB", "model_name": "Samsung_SSD_860_EVO_500GB",
"interface_type": "SCSI", "interface_type": "SCSI",
@@ -0,0 +1,61 @@
<div [ngClass]="{ 'border-green': deviceSummary.device.device_status == 0 && deviceSummary.smart,
'border-red': deviceSummary.device.device_status != 0 }"
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">
<mat-icon class="icon-size-96 opacity-12 text-green"
*ngIf="deviceSummary.device.device_status == 0 && deviceSummary.smart"
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-red"
*ngIf="deviceSummary.device.device_status != 0"
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-yellow"
*ngIf="!deviceSummary.smart"
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
</div>
<div class="flex items-center">
<div class="flex flex-col">
<a [routerLink]="'/device/'+ deviceSummary.device.wwn"
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceTitle(deviceSummary.device)}}</a>
<div [ngClass]="classDeviceLastUpdatedOn(deviceSummary)" class="font-medium text-sm" *ngIf="deviceSummary.smart">
Last Updated on {{deviceSummary.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
</div>
</div>
<div class="ml-auto" *ngIf="deviceSummary.device">
<button mat-icon-button
[matMenuTriggerFor]="previousStatementMenu">
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
</button>
<mat-menu #previousStatementMenu="matMenu">
<a mat-menu-item [routerLink]="'/device/'+ deviceSummary.device.wwn">
<span class="flex items-center">
<mat-icon class="icon-size-20 mr-3"
[svgIcon]="'payment'"></mat-icon>
<span>View Details</span>
</span>
</a>
</mat-menu>
</div>
</div>
<div class="flex flex-row flex-wrap mt-4 -mx-6">
<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="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.collector_date; else unknownStatus">{{ deviceStatusString(deviceSummary.device.device_status) | titlecase}}</div>
<ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
</div>
<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">Temperature</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.collector_date; else unknownTemp">{{ deviceSummary.smart?.temp }}°C</div>
<ng-template #unknownTemp><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
</div>
<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">Capacity</div>
<div class="mt-2 font-medium text-3xl leading-none">{{ deviceSummary.device.capacity | fileSize}}</div>
</div>
<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="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>
<ng-template #unknownPoweredOn><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
</div>
</div>
</div>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardDeviceComponent } from './dashboard-device.component';
describe('DashboardDeviceComponent', () => {
let component: DashboardDeviceComponent;
let fixture: ComponentFixture<DashboardDeviceComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardDeviceComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardDeviceComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,115 @@
import {Component, Input, OnInit} from '@angular/core';
import * as moment from "moment";
import {takeUntil} from "rxjs/operators";
import {AppConfig} from "app/core/config/app.config";
import {TreoConfigService} from "@treo/services/config";
import {Subject} from "rxjs";
import humanizeDuration from 'humanize-duration'
@Component({
selector: 'app-dashboard-device',
templateUrl: './dashboard-device.component.html',
styleUrls: ['./dashboard-device.component.scss']
})
export class DashboardDeviceComponent implements OnInit {
@Input() deviceSummary: any;
@Input() deviceWWN: string;
config: AppConfig;
private _unsubscribeAll: Subject<any>;
constructor(
private _configService: TreoConfigService,
) {
// Set the private defaults
this._unsubscribeAll = new Subject();
}
ngOnInit(): void {
// Subscribe to config changes
this._configService.config$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((config: AppConfig) => {
this.config = config;
});
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
classDeviceLastUpdatedOn(deviceSummary){
if (deviceSummary.device.device_status !== 0) {
return 'text-red' // if the device has failed, always highlight in red
} else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){
if(moment().subtract(14, 'd').isBefore(deviceSummary.smart.collector_date)){
// this device was updated in the last 2 weeks.
return 'text-green'
} else if(moment().subtract(1, 'm').isBefore(deviceSummary.smart.collector_date)){
// this device was updated in the last month
return 'text-yellow'
} else{
// last updated more than a month ago.
return 'text-red'
}
} else {
return ''
}
}
deviceTitle(disk){
console.log(`Displaying Device ${disk.wwn} with: ${this.config.dashboardDisplay}`)
let titleParts = []
if (disk.host_id) titleParts.push(disk.host_id)
//add device identifier (fallback to generated device name)
titleParts.push(deviceDisplayTitle(disk, this.config.dashboardDisplay) || deviceDisplayTitle(disk, 'name'))
return titleParts.join(' - ')
}
deviceStatusString(deviceStatus){
if(deviceStatus == 0){
return "passed"
} else {
return "failed"
}
}
readonly humanizeDuration = humanizeDuration;
}
export function deviceDisplayTitle(disk, titleType: string){
let titleParts = []
switch(titleType){
case 'name':
titleParts.push(`/dev/${disk.device_name}`)
if (disk.device_type && disk.device_type != 'scsi' && disk.device_type != 'ata'){
titleParts.push(disk.device_type)
}
titleParts.push(disk.model_name)
break;
case 'serial_id':
if(!disk.device_serial_id) return ''
titleParts.push(`/by-id/${disk.device_serial_id}`)
break;
case 'uuid':
if(!disk.device_uuid) return ''
titleParts.push(`/by-uuid/${disk.device_uuid}`)
break;
case 'label':
if(disk.label){
titleParts.push(disk.label)
} else if(disk.device_label){
titleParts.push(`/by-label/${disk.device_label}`)
}
break;
}
return titleParts.join(' - ')
}
@@ -0,0 +1,52 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { Overlay } from '@angular/cdk/overlay';
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { SharedModule } from 'app/shared/shared.module';
import {DashboardDeviceComponent} from 'app/layout/common/dashboard-device/dashboard-device.component'
import { MatDialogModule } from "@angular/material/dialog";
import { MatButtonToggleModule} from "@angular/material/button-toggle";
import {MatTabsModule} from "@angular/material/tabs";
import {MatSliderModule} from "@angular/material/slider";
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
import {MatTooltipModule} from "@angular/material/tooltip";
import {dashboardRoutes} from "../../../modules/dashboard/dashboard.routing";
import {MatDividerModule} from "@angular/material/divider";
import {MatMenuModule} from "@angular/material/menu";
import {MatProgressBarModule} from "@angular/material/progress-bar";
import {MatSortModule} from "@angular/material/sort";
import {MatTableModule} from "@angular/material/table";
import {NgApexchartsModule} from "ng-apexcharts";
import {DashboardSettingsModule} from "../dashboard-settings/dashboard-settings.module";
@NgModule({
declarations: [
DashboardDeviceComponent
],
imports : [
RouterModule.forChild([]),
RouterModule.forChild(dashboardRoutes),
MatButtonModule,
MatDividerModule,
MatTooltipModule,
MatIconModule,
MatMenuModule,
MatProgressBarModule,
MatSortModule,
MatTableModule,
NgApexchartsModule,
SharedModule,
],
exports : [
DashboardDeviceComponent,
],
providers : []
})
export class DashboardDeviceModule
{
}
@@ -1,14 +1,23 @@
<h2 mat-dialog-title>Scrutiny Settings</h2> <h2 mat-dialog-title>Scrutiny Settings</h2>
<mat-dialog-content class="mat-typography"> <mat-dialog-content class="mat-typography">
<form class="flex flex-col p-8 pb-0 overflow-hidden"> <div class="flex flex-col p-8 pb-0 overflow-hidden">
<div class="flex flex-col gt-xs:flex-row"> <div class="flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-xs:pr-3"> <mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
<mat-label>Display Title</mat-label>
<mat-select [(ngModel)]="dashboardDisplay">
<mat-option value="name">Name</mat-option>
<mat-option value="serial_id">Serial ID</mat-option>
<mat-option value="uuid">UUID</mat-option>
<mat-option value="label">Label</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pl-3">
<mat-label>Sort By</mat-label> <mat-label>Sort By</mat-label>
<mat-select [value]="'status'"> <mat-select [(ngModel)]="dashboardSort">
<mat-option value="status">Status</mat-option> <mat-option value="status">Status</mat-option>
<mat-option value="name" disabled>Name</mat-option> <mat-option value="title">Title</mat-option>
<mat-option value="label" disabled>Label</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@@ -17,61 +26,61 @@
<mat-tab-group mat-align-tabs="start"> <mat-tab-group mat-align-tabs="start">
<mat-tab label="Ata"> <mat-tab label="Ata">
<div class="flex flex-col mt-5 gt-md:flex-row"> <div matTooltip="not yet implemented" class="gray-200 flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-md:pr-3"> <mat-form-field class="flex-auto gt-md:pr-3">
<mat-label>Critical Error Threshold</mat-label> <mat-label>Critical Error Threshold</mat-label>
<input matInput [value]="'10%'"> <input disabled matInput [value]="'10%'">
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-auto gt-md:pl-3"> <mat-form-field class="flex-auto gt-md:pl-3">
<mat-label>Critical Warning Threshold</mat-label> <mat-label>Critical Warning Threshold</mat-label>
<input matInput> <input disabled matInput>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="flex flex-col gt-md:flex-row"> <div matTooltip="not yet implemented" class="gray-200 flex flex-col gt-md:flex-row">
<mat-form-field class="flex-auto gt-md:pr-3"> <mat-form-field class="flex-auto gt-md:pr-3">
<mat-label>Error Threshold</mat-label> <mat-label>Error Threshold</mat-label>
<input matInput [value]="'20%'"> <input disabled matInput [value]="'20%'">
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-auto gt-md:pl-3"> <mat-form-field class="flex-auto gt-md:pl-3">
<mat-label>Warning Threshold</mat-label> <mat-label>Warning Threshold</mat-label>
<input matInput [value]="'10%'"> <input disabled matInput [value]="'10%'">
</mat-form-field> </mat-form-field>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab label="NVMe"> <mat-tab label="NVMe">
<div class="flex flex-col mt-5 gt-md:flex-row"> <div matTooltip="not yet implemented" class="gray-200 flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-md:pr-3"> <mat-form-field class="flex-auto gt-md:pr-3">
<mat-label>Critical Error Threshold</mat-label> <mat-label>Critical Error Threshold</mat-label>
<input matInput [value]="'enabled'"> <input disabled matInput [value]="'enabled'">
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-auto gt-md:pl-3"> <mat-form-field class="flex-auto gt-md:pl-3">
<mat-label>Critical Warning Threshold</mat-label> <mat-label>Critical Warning Threshold</mat-label>
<input matInput> <input disabled matInput>
</mat-form-field> </mat-form-field>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab label="SCSI"> <mat-tab label="SCSI">
<div class="flex flex-col mt-5 gt-md:flex-row"> <div matTooltip="not yet implemented" class="gray-200 flex flex-col mt-5 gt-md:flex-row">
<mat-form-field class="flex-auto gt-md:pr-3"> <mat-form-field class="flex-auto gt-md:pr-3">
<mat-label>Critical Error Threshold</mat-label> <mat-label>Critical Error Threshold</mat-label>
<input matInput [value]="'enabled'"> <input disabled matInput [value]="'enabled'">
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-auto gt-md:pl-3"> <mat-form-field class="flex-auto gt-md:pl-3">
<mat-label>Critical Warning Threshold</mat-label> <mat-label>Critical Warning Threshold</mat-label>
<input matInput> <input disabled matInput>
</mat-form-field> </mat-form-field>
</div> </div>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
</div> </div>
</form> </div>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions align="end"> <mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button> <button mat-button mat-dialog-close>Cancel</button>
<button mat-button matTooltip="not yet implemented" [mat-dialog-close]="true" cdkFocusInitial>Save</button> <button mat-button mat-dialog-close (click)="saveSettings()" cdkFocusInitial>Save</button>
</mat-dialog-actions> </mat-dialog-actions>
@@ -1,4 +1,8 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import {AppConfig} from 'app/core/config/app.config';
import { TreoConfigService } from '@treo/services/config';
import {Subject} from "rxjs";
import {takeUntil} from "rxjs/operators";
@Component({ @Component({
selector: 'app-dashboard-settings', selector: 'app-dashboard-settings',
@@ -7,10 +11,41 @@ import { Component, OnInit } from '@angular/core';
}) })
export class DashboardSettingsComponent implements OnInit { export class DashboardSettingsComponent implements OnInit {
constructor() { } dashboardDisplay: string;
dashboardSort: string;
// Private
private _unsubscribeAll: Subject<any>;
constructor(
private _configService: TreoConfigService,
) {
// Set the private defaults
this._unsubscribeAll = new Subject();
}
ngOnInit(): void { ngOnInit(): void {
// Subscribe to config changes
this._configService.config$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((config: AppConfig) => {
// Store the config
this.dashboardDisplay = config.dashboardDisplay;
this.dashboardSort = config.dashboardSort;
});
} }
saveSettings(): void {
var newSettings = {
dashboardDisplay: this.dashboardDisplay,
dashboardSort: this.dashboardSort
}
this._configService.config = newSettings
console.log(`Saved Settings: ${JSON.stringify(newSettings)}`)
}
formatLabel(value: number) { formatLabel(value: number) {
return value; return value;
} }
@@ -47,71 +47,15 @@
</div> </div>
</div> </div>
<div class="flex flex-wrap w-full">
<div *ngFor="let summary of data.data.summary | keyvalue" class="flex gt-sm:w-1/2 min-w-80 p-4"> <div class="flex flex-wrap w-full" *ngFor="let hostId of hostGroups | keyvalue">
<div [ngClass]="{ 'border-green': summary.value.device.device_status == 0 && summary.value.smart, <h3 class="ml-4" *ngIf="hostId.key">{{hostId.key}}</h3>
'border-red': summary.value.device.device_status != 0 }" <div class="flex flex-wrap w-full">
class="relative flex flex-col flex-auto p-6 pr-3 pb-3 bg-card rounded border-l-4 shadow-md overflow-hidden"> <app-dashboard-device class="flex gt-sm:w-1/2 min-w-80 p-4" *ngFor="let deviceSummary of (deviceSummariesForHostGroup(hostId.value) | deviceSort:config.dashboardSort:config.dashboardDisplay )" [deviceWWN]="deviceSummary.device.wwn" [deviceSummary]="deviceSummary"></app-dashboard-device>
<div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
<mat-icon class="icon-size-96 opacity-12 text-green"
*ngIf="summary.value.device.device_status == 0 && summary.value.smart"
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-red"
*ngIf="summary.value.device.device_status != 0"
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
<mat-icon class="icon-size-96 opacity-12 text-yellow"
*ngIf="!summary.value.smart"
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
</div>
<div class="flex items-center">
<div class="flex flex-col">
<a [routerLink]="'/device/'+ summary.value.device.wwn"
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceTitle(summary.value.device)}}</a>
<div [ngClass]="classDeviceLastUpdatedOn(summary.value)" class="font-medium text-sm" *ngIf="summary.value.smart">
Last Updated on {{summary.value.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
</div>
</div>
<div class="ml-auto" *ngIf="summary.value.device">
<button mat-icon-button
[matMenuTriggerFor]="previousStatementMenu">
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
</button>
<mat-menu #previousStatementMenu="matMenu">
<a mat-menu-item [routerLink]="'/device/'+ summary.value.device.wwn">
<span class="flex items-center">
<mat-icon class="icon-size-20 mr-3"
[svgIcon]="'payment'"></mat-icon>
<span>View Details</span>
</span>
</a>
</mat-menu>
</div>
</div>
<div class="flex flex-row flex-wrap mt-4 -mx-6">
<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="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.collector_date; else unknownStatus">{{ deviceStatusString(summary.value.device.device_status) | titlecase}}</div>
<ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
</div>
<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">Temperature</div>
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.collector_date; else unknownTemp">{{ summary.value.smart?.temp }}°C</div>
<ng-template #unknownTemp><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
</div>
<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">Capacity</div>
<div class="mt-2 font-medium text-3xl leading-none">{{ summary.value.device.capacity | fileSize}}</div>
</div>
<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="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.power_on_hours; else unknownPoweredOn">{{ humanizeDuration(summary.value.smart?.power_on_hours * 60 * 60 * 1000, { 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>
</div>
</div>
</div>
</div> </div>
</div> </div>
<!-- Drive Temperatures --> <!-- Drive Temperatures -->
<div class="flex flex-auto w-full min-w-80 h-90 p-4"> <div class="flex flex-auto w-full min-w-80 h-90 p-4">
<div class="flex flex-col flex-auto bg-card shadow-md rounded overflow-hidden"> <div class="flex flex-col flex-auto bg-card shadow-md rounded overflow-hidden">
@@ -123,22 +67,22 @@
</div> </div>
<div> <div>
<button class="h-8 min-h-8 px-2" <button class="h-8 min-h-8 px-2"
matTooltip="not yet implemented"
mat-button mat-button
[matMenuTriggerFor]="tempRangeMenu"> [matMenuTriggerFor]="tempRangeMenu">
<span class="font-medium text-sm text-hint">1 week</span> <span class="font-medium text-sm text-hint">{{tempDurationKey}}</span>
</button> </button>
<mat-menu #tempRangeMenu="matMenu"> <mat-menu #tempRangeMenu="matMenu">
<button mat-menu-item>1 month</button> <button (click)="changeSummaryTempDuration('forever')" mat-menu-item>forever</button>
<button mat-menu-item>12 months</button> <button (click)="changeSummaryTempDuration('year')" mat-menu-item>year</button>
<button mat-menu-item>all time</button> <button (click)="changeSummaryTempDuration('month')" mat-menu-item>month</button>
<button (click)="changeSummaryTempDuration('week')" mat-menu-item>week</button>
</mat-menu> </mat-menu>
</div> </div>
</div> </div>
</div> </div>
<div class="flex flex-col flex-auto"> <div class="flex flex-col flex-auto">
<apx-chart *ngIf="temperatureOptions" class="flex-auto w-full h-full" <apx-chart #tempChart *ngIf="temperatureOptions" class="flex-auto w-full h-full"
[chart]="temperatureOptions.chart" [chart]="temperatureOptions.chart"
[colors]="temperatureOptions.colors" [colors]="temperatureOptions.colors"
[fill]="temperatureOptions.fill" [fill]="temperatureOptions.fill"
@@ -3,12 +3,14 @@ import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { ApexOptions } from 'ng-apexcharts'; import {ApexOptions, ChartComponent} from 'ng-apexcharts';
import { DashboardService } from 'app/modules/dashboard/dashboard.service'; import { DashboardService } from 'app/modules/dashboard/dashboard.service';
import * as moment from "moment";
import {MatDialog} from '@angular/material/dialog'; import {MatDialog} from '@angular/material/dialog';
import { DashboardSettingsComponent } from 'app/layout/common/dashboard-settings/dashboard-settings.component'; import { DashboardSettingsComponent } from 'app/layout/common/dashboard-settings/dashboard-settings.component';
import humanizeDuration from 'humanize-duration' import {deviceDisplayTitle} from "app/layout/common/dashboard-device/dashboard-device.component";
import {AppConfig} from "app/core/config/app.config";
import {TreoConfigService} from "@treo/services/config";
import {Router} from "@angular/router";
@Component({ @Component({
selector : 'example', selector : 'example',
@@ -20,10 +22,14 @@ import humanizeDuration from 'humanize-duration'
export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
{ {
data: any; data: any;
hostGroups: { [hostId: string]: string[] } = {}
temperatureOptions: ApexOptions; temperatureOptions: ApexOptions;
tempDurationKey: string = "forever"
config: AppConfig;
// Private // Private
private _unsubscribeAll: Subject<any>; private _unsubscribeAll: Subject<any>;
@ViewChild("tempChart", { static: false }) tempChart: ChartComponent;
/** /**
* Constructor * Constructor
@@ -32,7 +38,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
*/ */
constructor( constructor(
private _smartService: DashboardService, private _smartService: DashboardService,
public dialog: MatDialog private _configService: TreoConfigService,
public dialog: MatDialog,
private router: Router,
) )
{ {
// Set the private defaults // Set the private defaults
@@ -49,6 +57,28 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
*/ */
ngOnInit(): void ngOnInit(): void
{ {
// Subscribe to config changes
this._configService.config$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((config: AppConfig) => {
//check if the old config and the new config do not match.
let oldConfig = JSON.stringify(this.config)
let newConfig = JSON.stringify(config)
if(oldConfig != newConfig){
console.log(`Configuration updated: ${newConfig} vs ${oldConfig}`)
// Store the config
this.config = config;
if(oldConfig){
console.log("reloading component...")
this.refreshComponent()
}
}
});
// Get the data // Get the data
this._smartService.data$ this._smartService.data$
.pipe(takeUntil(this._unsubscribeAll)) .pipe(takeUntil(this._unsubscribeAll))
@@ -57,6 +87,15 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
// Store the data // Store the data
this.data = data; this.data = data;
//generate group data.
for(let wwn in this.data.data.summary){
let hostid = this.data.data.summary[wwn].device.host_id
let hostDeviceList = this.hostGroups[hostid] || []
hostDeviceList.push(wwn)
this.hostGroups[hostid] = hostDeviceList
}
console.log(this.hostGroups)
// Prepare the chart data // Prepare the chart data
this._prepareChartData(); this._prepareChartData();
}); });
@@ -81,6 +120,14 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
// @ Private methods // @ Private methods
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
private refreshComponent(){
let currentUrl = this.router.url;
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.router.onSameUrlNavigation = 'reload';
this.router.navigate([currentUrl]);
}
private _deviceDataTemperatureSeries() { private _deviceDataTemperatureSeries() {
var deviceTemperatureSeries = [] var deviceTemperatureSeries = []
@@ -91,8 +138,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
if (!deviceSummary.temp_history){ if (!deviceSummary.temp_history){
continue continue
} }
let deviceName = this.deviceTitle(deviceSummary.device)
var deviceSeriesMetadata = { var deviceSeriesMetadata = {
name: `/dev/${deviceSummary.device.device_name}`, name: deviceName,
data: [] data: []
} }
@@ -164,6 +214,26 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
// @ Public methods // @ Public methods
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
deviceTitle(disk){
console.log(`Displaying Device ${disk.wwn} with: ${this.config.dashboardDisplay}`)
let titleParts = []
if (disk.host_id) titleParts.push(disk.host_id)
//add device identifier (fallback to generated device name)
titleParts.push(deviceDisplayTitle(disk, this.config.dashboardDisplay) || deviceDisplayTitle(disk, 'name'))
return titleParts.join(' - ')
}
deviceSummariesForHostGroup(hostGroupWWNs: string[]) {
let deviceSummaries = []
for(let wwn of hostGroupWWNs){
deviceSummaries.push(this.data.data.summary[wwn])
}
return deviceSummaries
}
openDialog() { openDialog() {
const dialogRef = this.dialog.open(DashboardSettingsComponent); const dialogRef = this.dialog.open(DashboardSettingsComponent);
@@ -172,48 +242,29 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
}); });
} }
deviceTitle(disk){ /*
let title = []
if (disk.host_id) title.push(disk.host_id) DURATION_KEY_WEEK = "week"
DURATION_KEY_MONTH = "month"
DURATION_KEY_YEAR = "year"
DURATION_KEY_FOREVER = "forever"
*/
title.push(`/dev/${disk.device_name}`) changeSummaryTempDuration(durationKey: string){
this.tempDurationKey = durationKey
if (disk.device_type && disk.device_type != 'scsi' && disk.device_type != 'ata'){ this._smartService.getSummaryTempData(durationKey)
title.push(disk.device_type) .subscribe((data) => {
}
title.push(disk.model_name) // given a list of device temp history, override the data in the "summary" object.
for(const wwn in this.data.data.summary) {
// console.log(`Updating ${wwn}, length: ${this.data.data.summary[wwn].temp_history.length}`)
this.data.data.summary[wwn].temp_history = data.data.temp_history[wwn] || []
}
return title.join(' - ') // Prepare the chart series data
} this.tempChart.updateSeries(this._deviceDataTemperatureSeries())
});
deviceStatusString(deviceStatus){
if(deviceStatus == 0){
return "passed"
} else {
return "failed"
}
}
classDeviceLastUpdatedOn(deviceSummary){
if (deviceSummary.device.device_status !== 0) {
return 'text-red' // if the device has failed, always highlight in red
} else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){
if(moment().subtract(14, 'd').isBefore(deviceSummary.smart.collector_date)){
// this device was updated in the last 2 weeks.
return 'text-green'
} else if(moment().subtract(1, 'm').isBefore(deviceSummary.smart.collector_date)){
// this device was updated in the last month
return 'text-yellow'
} else{
// last updated more than a month ago.
return 'text-red'
}
} else {
return ''
}
} }
/** /**
@@ -227,6 +278,4 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
return item.id || index; return item.id || index;
} }
readonly humanizeDuration = humanizeDuration;
} }
@@ -13,12 +13,13 @@ import { MatTableModule } from '@angular/material/table';
import { NgApexchartsModule } from 'ng-apexcharts'; import { NgApexchartsModule } from 'ng-apexcharts';
import { MatTooltipModule } from '@angular/material/tooltip' import { MatTooltipModule } from '@angular/material/tooltip'
import { DashboardSettingsModule } from "app/layout/common/dashboard-settings/dashboard-settings.module"; import { DashboardSettingsModule } from "app/layout/common/dashboard-settings/dashboard-settings.module";
import { DashboardDeviceModule } from "app/layout/common/dashboard-device/dashboard-device.module";
@NgModule({ @NgModule({
declarations: [ declarations: [
DashboardComponent DashboardComponent
], ],
imports : [ imports: [
RouterModule.forChild(dashboardRoutes), RouterModule.forChild(dashboardRoutes),
MatButtonModule, MatButtonModule,
MatDividerModule, MatDividerModule,
@@ -30,7 +31,8 @@ import { DashboardSettingsModule } from "app/layout/common/dashboard-settings/da
MatTableModule, MatTableModule,
NgApexchartsModule, NgApexchartsModule,
SharedModule, SharedModule,
DashboardSettingsModule DashboardSettingsModule,
DashboardDeviceModule
] ]
}) })
export class DashboardModule export class DashboardModule
@@ -31,6 +31,6 @@ export class DashboardResolver implements Resolve<any>
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any>
{ {
return this._dashboardService.getData(); return this._dashboardService.getSummaryData();
} }
} }
@@ -44,7 +44,7 @@ export class DashboardService
/** /**
* Get data * Get data
*/ */
getData(): Observable<any> getSummaryData(): Observable<any>
{ {
return this._httpClient.get(getBasePath() + '/api/summary').pipe( return this._httpClient.get(getBasePath() + '/api/summary').pipe(
tap((response: any) => { tap((response: any) => {
@@ -52,4 +52,14 @@ export class DashboardService
}) })
); );
} }
getSummaryTempData(durationKey: string): Observable<any>
{
let params = {}
if(durationKey){
params["duration_key"] = durationKey
}
return this._httpClient.get(getBasePath() + '/api/summary/temp', {params: params});
}
} }
@@ -1,33 +1,60 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import {deviceDisplayTitle} from "app/layout/common/dashboard-device/dashboard-device.component";
@Pipe({ @Pipe({
name: 'deviceSort' name: 'deviceSort'
}) })
export class DeviceSortPipe implements PipeTransform { export class DeviceSortPipe implements PipeTransform {
numericalStatus(device): number { statusCompareFn(a: any, b: any) {
if(!device.smart_results[0]){ function deviceStatus(deviceSummary): number {
return 0 if(!deviceSummary.smart){
} else if (device.smart_results[0].smart_status == 'passed'){ return 0
return 1 } else if (deviceSummary.device.device_status == 0){
} else { return 1
return -1 } else {
return deviceSummary.device.device_status * -1 // will return range from -1, -2, -3
}
}
let left = deviceStatus(a)
let right = deviceStatus(b)
return left - right;
}
titleCompareFn(dashboardDisplay: string) {
return function (a: any, b: any){
let _dashboardDisplay = dashboardDisplay
let left = deviceDisplayTitle(a.device, _dashboardDisplay) || deviceDisplayTitle(a.device, 'name')
let right = deviceDisplayTitle(b.device, _dashboardDisplay) || deviceDisplayTitle(b.device, 'name')
if( left < right )
return -1;
if( left > right )
return 1;
return 0;
} }
} }
transform(devices: Array<unknown>, ...args: unknown[]): Array<unknown> { transform(deviceSummaries: Array<unknown>, sortBy = 'status', dashboardDisplay = "name"): Array<unknown> {
let compareFn = undefined
switch (sortBy) {
case 'status':
compareFn = this.statusCompareFn
break;
case 'title':
compareFn = this.titleCompareFn(dashboardDisplay)
break;
}
//failed, unknown/empty, passed //failed, unknown/empty, passed
devices.sort((a: any, b: any) => { deviceSummaries.sort(compareFn);
let left = this.numericalStatus(a) return deviceSummaries;
let right = this.numericalStatus(b)
return left - right;
});
return devices;
} }
} }