From 999c12748c6751f3f94aa193c23c12d5365d13ad Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 9 May 2022 18:14:08 -0700 Subject: [PATCH 01/14] added a way to retrieve raw udev data. Can be used to retrieve disk label, UUID and "disk/by-id/*" device info. Storing it in the database during device registration. --- collector/pkg/detect/devices_darwin.go | 2 +- collector/pkg/detect/devices_linux.go | 52 +++++++++++++++++++ collector/pkg/models/device.go | 11 +++- .../migrations/m20220503120000/device.go | 1 + .../database/scrutiny_repository_device.go | 2 +- .../scrutiny_repository_migrations.go | 12 ++++- webapp/backend/pkg/models/device.go | 4 ++ 7 files changed, 79 insertions(+), 5 deletions(-) diff --git a/collector/pkg/detect/devices_darwin.go b/collector/pkg/detect/devices_darwin.go index 928a38d..62a6ba2 100644 --- a/collector/pkg/detect/devices_darwin.go +++ b/collector/pkg/detect/devices_darwin.go @@ -13,7 +13,7 @@ func DevicePrefix() string { func (d *Detect) Start() ([]models.Device, error) { 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() if err != nil { return nil, err diff --git a/collector/pkg/detect/devices_linux.go b/collector/pkg/detect/devices_linux.go index ebe8e88..0c4c7a0 100644 --- a/collector/pkg/detect/devices_linux.go +++ b/collector/pkg/detect/devices_linux.go @@ -1,9 +1,12 @@ package detect import ( + "fmt" "github.com/analogj/scrutiny/collector/pkg/common/shell" "github.com/analogj/scrutiny/collector/pkg/models" "github.com/jaypipes/ghw" + "io/ioutil" + "path/filepath" "strings" ) @@ -22,6 +25,7 @@ func (d *Detect) Start() ([]models.Device, error) { //inflate device info for detected devices. for ndx, _ := range detectedDevices { d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors. + populateUdevInfo(&detectedDevices[ndx]) //ignore errors. } return detectedDevices, nil @@ -49,3 +53,51 @@ func (d *Detect) wwnFallback(detectedDevice *models.Device) { //wwn must always be lowercase. 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 +} + diff --git a/collector/pkg/models/device.go b/collector/pkg/models/device.go index dd77794..f06aec8 100644 --- a/collector/pkg/models/device.go +++ b/collector/pkg/models/device.go @@ -1,10 +1,13 @@ package models type Device struct { - WWN string `json:"wwn"` - HostId string `json:"host_id"` + WWN string `json:"wwn"` 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"` @@ -17,6 +20,10 @@ type Device struct { 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"` } type DeviceWrapper struct { diff --git a/webapp/backend/pkg/database/migrations/m20220503120000/device.go b/webapp/backend/pkg/database/migrations/m20220503120000/device.go index bbcdebd..72dd27f 100644 --- a/webapp/backend/pkg/database/migrations/m20220503120000/device.go +++ b/webapp/backend/pkg/database/migrations/m20220503120000/device.go @@ -5,6 +5,7 @@ import ( "time" ) +// Deprecated: m20220503120000.Device is deprecated, only used by db migrations type Device struct { //GORM attributes, see: http://gorm.io/docs/conventions.html CreatedAt time.Time diff --git a/webapp/backend/pkg/database/scrutiny_repository_device.go b/webapp/backend/pkg/database/scrutiny_repository_device.go index 27346f3..ed7d22d 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_device.go +++ b/webapp/backend/pkg/database/scrutiny_repository_device.go @@ -18,7 +18,7 @@ import ( func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev models.Device) error { if err := sr.gormClient.WithContext(ctx).Clauses(clause.OnConflict{ 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 { return err } diff --git a/webapp/backend/pkg/database/scrutiny_repository_migrations.go b/webapp/backend/pkg/database/scrutiny_repository_migrations.go index e3e902a..47aeb23 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_migrations.go +++ b/webapp/backend/pkg/database/scrutiny_repository_migrations.go @@ -5,6 +5,7 @@ import ( "fmt" "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/m20220509170100" "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/measurements" @@ -257,10 +258,19 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error { return err } - //migrate the device database to the current version + //migrate the device database 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 { diff --git a/webapp/backend/pkg/models/device.go b/webapp/backend/pkg/models/device.go index c5272b4..28dd150 100644 --- a/webapp/backend/pkg/models/device.go +++ b/webapp/backend/pkg/models/device.go @@ -21,6 +21,10 @@ type Device struct { 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"` From 1bfdd0043ff263b34be3572891b1c6b422d1f504 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 9 May 2022 18:14:43 -0700 Subject: [PATCH 02/14] added a way to retrieve raw udev data. Can be used to retrieve disk label, UUID and "disk/by-id/*" device info. Storing it in the database during device registration. --- .../migrations/m20220509170100/device.go | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 webapp/backend/pkg/database/migrations/m20220509170100/device.go diff --git a/webapp/backend/pkg/database/migrations/m20220509170100/device.go b/webapp/backend/pkg/database/migrations/m20220509170100/device.go new file mode 100644 index 0000000..1134fff --- /dev/null +++ b/webapp/backend/pkg/database/migrations/m20220509170100/device.go @@ -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"` +} + From 3b137964fc1b1b10912e9f91321e9d0df1cf6ec7 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Tue, 10 May 2022 09:19:40 -0700 Subject: [PATCH 03/14] make sure we include the host id in the temp history label. --- webapp/frontend/src/app/data/mock/summary/data.ts | 2 +- .../src/app/modules/dashboard/dashboard.component.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/webapp/frontend/src/app/data/mock/summary/data.ts b/webapp/frontend/src/app/data/mock/summary/data.ts index 70acf16..801497d 100644 --- a/webapp/frontend/src/app/data/mock/summary/data.ts +++ b/webapp/frontend/src/app/data/mock/summary/data.ts @@ -138,7 +138,7 @@ export const summary = { "device_protocol": "", "device_type": "", "label": "", - "host_id": "", + "host_id": "custom host id", "device_status": 1 }, "smart": { diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts index a0d6738..2fdbf0b 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts @@ -91,8 +91,14 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy if (!deviceSummary.temp_history){ continue } + + let deviceName = `/dev/${deviceSummary.device.device_name}` + if(deviceSummary.device.host_id){ + deviceName = `${deviceSummary.device.host_id} - ${deviceName}` + } + var deviceSeriesMetadata = { - name: `/dev/${deviceSummary.device.device_name}`, + name: deviceName, data: [] } From 5899bf2026018cbac50add0cf28a8112b0795455 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Thu, 12 May 2022 07:35:38 -0700 Subject: [PATCH 04/14] started working on Dashboard UI sorting and naming --- .../src/app/core/config/app.config.ts | 10 +++- .../src/app/data/mock/summary/data.ts | 21 ++++++++ .../dashboard-settings.component.html | 22 ++++++-- .../dashboard-settings.component.ts | 35 ++++++++++++- .../modules/dashboard/dashboard.component.ts | 50 ++++++++++++++++--- 5 files changed, 124 insertions(+), 14 deletions(-) diff --git a/webapp/frontend/src/app/core/config/app.config.ts b/webapp/frontend/src/app/core/config/app.config.ts index e996c1c..6dedd42 100644 --- a/webapp/frontend/src/app/core/config/app.config.ts +++ b/webapp/frontend/src/app/core/config/app.config.ts @@ -11,6 +11,11 @@ export interface AppConfig { theme: Theme; layout: Layout; + + // Dashboard options + dashboardDisplay: string; + dashboardSort: string; + } /** @@ -23,6 +28,9 @@ export interface AppConfig */ export const appConfig: AppConfig = { theme : "light", - layout: "material" + layout: "material", + + dashboardDisplay: "name", + dashboardSort: "status", }; diff --git a/webapp/frontend/src/app/data/mock/summary/data.ts b/webapp/frontend/src/app/data/mock/summary/data.ts index 801497d..e5f39bb 100644 --- a/webapp/frontend/src/app/data/mock/summary/data.ts +++ b/webapp/frontend/src/app/data/mock/summary/data.ts @@ -11,6 +11,9 @@ export const summary = { "DeletedAt": null, "wwn": "0x5000c500673e6b5f", "device_name": "sdg", + "device_label": "14TB-WD-DRIVE2", + "device_uuid": "", + "device_serial_id": "ata-ST6000DX000-1H217Z-Z4DXXXXX", "manufacturer": "ATA", "model_name": "ST6000DX000-1H217Z", "interface_type": "SCSI", @@ -35,6 +38,9 @@ export const summary = { "DeletedAt": null, "wwn": "0x5000cca252c859cc", "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", "model_name": "WDC_WD80EFAX-68LHPN0", "interface_type": "SCSI", @@ -68,6 +74,9 @@ export const summary = { "DeletedAt": null, "wwn": "0x5000cca264eb01d7", "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", "model_name": "WDC_WD140EDFZ-11A0VA0", "interface_type": "SCSI", @@ -101,6 +110,9 @@ export const summary = { "DeletedAt": null, "wwn": "0x5000cca264ebc248", "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", "model_name": "WDC_WD140EDFZ-11A0VA0", "interface_type": "SCSI", @@ -125,6 +137,9 @@ export const summary = { "DeletedAt": null, "wwn": "0x5000cca264ec3183", "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", "model_name": "WDC_WD140EDFZ-11A0VA0", "interface_type": "SCSI", @@ -542,6 +557,9 @@ export const summary = { "DeletedAt": null, "wwn": "0x50014ee20b2a72a9", "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", "model_name": "WDC_WD60EFRX-68MYMN1", "interface_type": "SCSI", @@ -566,6 +584,9 @@ export const summary = { "DeletedAt": null, "wwn": "0x5002538e40a22954", "device_name": "sda", + "device_label": "", + "device_uuid": "", + "device_serial_id": "ata-Samsung_SSD_860_EVO_500GB-S3YZNB0KBXXXXXX", "manufacturer": "ATA", "model_name": "Samsung_SSD_860_EVO_500GB", "interface_type": "SCSI", diff --git a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html index dddab9b..773ed26 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html +++ b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html @@ -2,12 +2,24 @@
-
- +
+ + Display + + Name + Serial ID + UUID + Label + + + + Sort By - - Status + + Status Name + Serial ID + UUID Label @@ -73,5 +85,5 @@ - + diff --git a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts index 4e1529d..dab49f7 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts @@ -1,4 +1,8 @@ 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({ selector: 'app-dashboard-settings', @@ -7,10 +11,39 @@ import { Component, OnInit } from '@angular/core'; }) export class DashboardSettingsComponent implements OnInit { - constructor() { } + dashboardDisplay: string; + dashboardSort: string; + + // Private + private _unsubscribeAll: Subject; + + 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) => { + + // Store the config + this.dashboardDisplay = config.dashboardDisplay; + this.dashboardSort = config.dashboardSort; + + }); } + + saveSettings(): void { + this._configService.config = { + dashboardDisplay: this.dashboardDisplay, + dashboardSort: this.dashboardSort + } + } + formatLabel(value: number) { return value; } diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts index 2fdbf0b..4cc9b0a 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts @@ -5,10 +5,12 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { ApexOptions } from 'ng-apexcharts'; import { DashboardService } from 'app/modules/dashboard/dashboard.service'; -import * as moment from "moment"; +import * as moment from 'moment'; import {MatDialog} from '@angular/material/dialog'; import { DashboardSettingsComponent } from 'app/layout/common/dashboard-settings/dashboard-settings.component'; import humanizeDuration from 'humanize-duration' +import {AppConfig} from 'app/core/config/app.config'; +import { TreoConfigService } from '@treo/services/config'; @Component({ selector : 'example', @@ -21,6 +23,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy { data: any; temperatureOptions: ApexOptions; + config: AppConfig; // Private private _unsubscribeAll: Subject; @@ -32,7 +35,8 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy */ constructor( private _smartService: DashboardService, - public dialog: MatDialog + public dialog: MatDialog, + private _configService: TreoConfigService, ) { // Set the private defaults @@ -49,6 +53,16 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy */ ngOnInit(): void { + // Subscribe to config changes + this._configService.config$ + .pipe(takeUntil(this._unsubscribeAll)) + .subscribe((config: AppConfig) => { + console.log('Configuration updated...') + // Store the config + this.config = config; + + }); + // Get the data this._smartService.data$ .pipe(takeUntil(this._unsubscribeAll)) @@ -180,16 +194,38 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy deviceTitle(disk){ let title = [] - + let showModelName = false if (disk.host_id) title.push(disk.host_id) - title.push(`/dev/${disk.device_name}`) + let deviceDisplay = '' + switch(this.config.dashboardDisplay){ + case 'name': + deviceDisplay = `/dev/${disk.device_name}` + if (disk.device_type && disk.device_type != 'scsi' && disk.device_type != 'ata'){ + title.push(disk.device_type) + } + showModelName = true - if (disk.device_type && disk.device_type != 'scsi' && disk.device_type != 'ata'){ - title.push(disk.device_type) + break; + case 'serial_id': + deviceDisplay = disk.device_serial_id + break; + case 'uuid': + deviceDisplay = disk.device_uuid + break; + case 'label': + deviceDisplay = disk.label || disk.device_label } - title.push(disk.model_name) + if(!deviceDisplay) { + // fallback + deviceDisplay = `/dev/${disk.device_name}` + } + + title.push(deviceDisplay) + if(showModelName){ + title.push(disk.model_name) + } return title.join(' - ') } From 0aeb13c181df3d359e20cfd1cb4833c9ad2ebc27 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Sat, 14 May 2022 17:54:19 -0700 Subject: [PATCH 05/14] support custom display of devices by UUID/ID/Label & Scrutiny Name. (Does not persist). Related #225 --- .../dashboard-settings.component.html | 8 +- .../dashboard-settings.component.ts | 4 +- .../modules/dashboard/dashboard.component.ts | 98 ++++++++++++------- 3 files changed, 70 insertions(+), 40 deletions(-) diff --git a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html index 773ed26..a90786b 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html +++ b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html @@ -1,11 +1,11 @@

Scrutiny Settings

- +
Display - + Name Serial ID UUID @@ -15,7 +15,7 @@ Sort By - + Status Name Serial ID @@ -80,7 +80,7 @@
- +
diff --git a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts index dab49f7..dc259fe 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts @@ -38,10 +38,12 @@ export class DashboardSettingsComponent implements OnInit { } saveSettings(): void { - this._configService.config = { + var newSettings = { dashboardDisplay: this.dashboardDisplay, dashboardSort: this.dashboardSort } + this._configService.config = newSettings + console.log(`Saved Settings: ${JSON.stringify(newSettings)}`) } formatLabel(value: number) { diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts index 4cc9b0a..21e7523 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts @@ -11,6 +11,7 @@ import { DashboardSettingsComponent } from 'app/layout/common/dashboard-settings import humanizeDuration from 'humanize-duration' import {AppConfig} from 'app/core/config/app.config'; import { TreoConfigService } from '@treo/services/config'; +import {Router, NavigationEnd,ActivatedRoute} from '@angular/router'; @Component({ selector : 'example', @@ -37,6 +38,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy private _smartService: DashboardService, public dialog: MatDialog, private _configService: TreoConfigService, + private router: Router, + private activatedRoute: ActivatedRoute + ) { // Set the private defaults @@ -57,10 +61,21 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy this._configService.config$ .pipe(takeUntil(this._unsubscribeAll)) .subscribe((config: AppConfig) => { - console.log('Configuration updated...') - // Store the config - this.config = config; + //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 @@ -95,6 +110,14 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy // ----------------------------------------------------------------------------------------------------- // @ Private methods // ----------------------------------------------------------------------------------------------------- + private refreshComponent(){ + + let currentUrl = this.router.url; + this.router.routeReuseStrategy.shouldReuseRoute = () => false; + this.router.onSameUrlNavigation = 'reload'; + this.router.navigate([currentUrl]); + } + private _deviceDataTemperatureSeries() { var deviceTemperatureSeries = [] @@ -180,6 +203,37 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy }; } + private _deviceDisplayTitle(disk, titleType: string){ + let deviceDisplay = '' + 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(' - ') + } + // ----------------------------------------------------------------------------------------------------- // @ Public methods // ----------------------------------------------------------------------------------------------------- @@ -193,41 +247,15 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy } deviceTitle(disk){ - let title = [] - let showModelName = false - if (disk.host_id) title.push(disk.host_id) - let deviceDisplay = '' - switch(this.config.dashboardDisplay){ - case 'name': - deviceDisplay = `/dev/${disk.device_name}` - if (disk.device_type && disk.device_type != 'scsi' && disk.device_type != 'ata'){ - title.push(disk.device_type) - } - showModelName = true + console.log(`Displaying Dashboard with: ${this.config.dashboardDisplay}`) + let titleParts = [] + if (disk.host_id) titleParts.push(disk.host_id) - break; - case 'serial_id': - deviceDisplay = disk.device_serial_id - break; - case 'uuid': - deviceDisplay = disk.device_uuid - break; - case 'label': - deviceDisplay = disk.label || disk.device_label - } + //add device identifier (fallback to generated device name) + titleParts.push(this._deviceDisplayTitle(disk, this.config.dashboardDisplay) || this._deviceDisplayTitle(disk, 'name')) - if(!deviceDisplay) { - // fallback - deviceDisplay = `/dev/${disk.device_name}` - } - - title.push(deviceDisplay) - if(showModelName){ - title.push(disk.model_name) - } - - return title.join(' - ') + return titleParts.join(' - ') } deviceStatusString(deviceStatus){ From 934f16f0a5efbe3ceb5ea90d08bea6e573a7bdd6 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 16 May 2022 10:02:14 -0700 Subject: [PATCH 06/14] persist settings across sessions (in local storage). --- .../@treo/services/config/config.service.ts | 21 ++++++++++++++++--- .../modules/dashboard/dashboard.component.ts | 5 +---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/webapp/frontend/src/@treo/services/config/config.service.ts b/webapp/frontend/src/@treo/services/config/config.service.ts index 97bd0ee..b1501f5 100644 --- a/webapp/frontend/src/@treo/services/config/config.service.ts +++ b/webapp/frontend/src/@treo/services/config/config.service.ts @@ -3,6 +3,8 @@ import { BehaviorSubject, Observable } from 'rxjs'; import * as _ from 'lodash'; import { TREO_APP_CONFIG } from '@treo/services/config/config.constants'; +const SCRUTINY_CONFIG_LOCAL_STORAGE_KEY = 'scrutiny'; + @Injectable({ providedIn: 'root' }) @@ -10,14 +12,22 @@ export class TreoConfigService { // Private private _config: BehaviorSubject; - /** * 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 - this._config = new BehaviorSubject(config); + this._config = new BehaviorSubject(currentScrutinyConfig); } // ----------------------------------------------------------------------------------------------------- @@ -27,15 +37,20 @@ export class TreoConfigService /** * Setter and getter for config */ + //Setter set config(value: any) { // Merge the new config over to the current config 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 this._config.next(config); } + //Getter get config$(): Observable { return this._config.asObservable(); diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts index 21e7523..8294c94 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts @@ -129,10 +129,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy continue } - let deviceName = `/dev/${deviceSummary.device.device_name}` - if(deviceSummary.device.host_id){ - deviceName = `${deviceSummary.device.host_id} - ${deviceName}` - } + let deviceName = this.deviceTitle(deviceSummary.device) var deviceSeriesMetadata = { name: deviceName, From 399a2450ffb75bbb2415097291d017ba176079e7 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 16 May 2022 19:19:36 -0700 Subject: [PATCH 07/14] make sure we can change the temperature duration key for the chart. --- .../dashboard/dashboard.component.html | 11 +++---- .../modules/dashboard/dashboard.component.ts | 29 ++++++++++++++++++- .../modules/dashboard/dashboard.resolvers.ts | 2 +- .../modules/dashboard/dashboard.service.ts | 12 +++++++- 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html index 8e321c4..7c517b9 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html @@ -126,19 +126,20 @@ matTooltip="not yet implemented" mat-button [matMenuTriggerFor]="tempRangeMenu"> - 1 week + {{tempDurationKey}} - - - + + + +
- ; + @ViewChild("tempChart", { static: false }) tempChart: ChartComponent; /** * Constructor @@ -283,6 +285,31 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy } } + /* + + DURATION_KEY_WEEK = "week" + DURATION_KEY_MONTH = "month" + DURATION_KEY_YEAR = "year" + DURATION_KEY_FOREVER = "forever" + */ + + changeSummaryTempDuration(durationKey: string){ + this.tempDurationKey = durationKey + + this._smartService.getSummaryTempData(durationKey) + .subscribe((data) => { + + // 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] || [] + } + + // Prepare the chart series data + this.tempChart.updateSeries(this._deviceDataTemperatureSeries()) + }); + } + /** * Track by function for ngFor loops * diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts index 419105b..eb29b48 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts @@ -31,6 +31,6 @@ export class DashboardResolver implements Resolve */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this._dashboardService.getData(); + return this._dashboardService.getSummaryData(); } } diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts index f73704c..ed91799 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts @@ -44,7 +44,7 @@ export class DashboardService /** * Get data */ - getData(): Observable + getSummaryData(): Observable { return this._httpClient.get(getBasePath() + '/api/summary').pipe( tap((response: any) => { @@ -52,4 +52,14 @@ export class DashboardService }) ); } + + getSummaryTempData(durationKey: string): Observable + { + let params = {} + if(durationKey){ + params["duration_key"] = durationKey + } + + return this._httpClient.get(getBasePath() + '/api/summary/temp', {params: params}); + } } From 743ce27d2ea131d25489fcc6c51df7272be52efb Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 16 May 2022 19:41:33 -0700 Subject: [PATCH 08/14] adding comment. --- .../frontend/src/app/modules/dashboard/dashboard.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts index 70617b0..a4ea071 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts @@ -247,7 +247,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy deviceTitle(disk){ - console.log(`Displaying Dashboard with: ${this.config.dashboardDisplay}`) + console.log(`Displaying Device ${disk.wwn} with: ${this.config.dashboardDisplay}`) let titleParts = [] if (disk.host_id) titleParts.push(disk.host_id) From 4190f9a6334650fbb5b903d81bb4f28a5cd9c008 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Tue, 17 May 2022 09:34:13 -0700 Subject: [PATCH 09/14] remove filter not implemented message. --- .../frontend/src/app/modules/dashboard/dashboard.component.html | 1 - 1 file changed, 1 deletion(-) diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html index 7c517b9..a642e32 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html @@ -123,7 +123,6 @@
+ + + + + View Details + + + +
+ +
+
+
Status
+
{{ deviceStatusString(deviceSummary.device.device_status) | titlecase}}
+
No Data
+
+
+
Temperature
+
{{ deviceSummary.smart?.temp }}°C
+
--
+
+
+
Capacity
+
{{ deviceSummary.device.capacity | fileSize}}
+
+
+
Powered On
+
{{ humanizeDuration(deviceSummary.smart?.power_on_hours * 60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] }) }}
+
--
+
+
+ + diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.scss b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.spec.ts b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.spec.ts new file mode 100644 index 0000000..dba412c --- /dev/null +++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DashboardDeviceComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DashboardDeviceComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts new file mode 100644 index 0000000..5cd83be --- /dev/null +++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts @@ -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; + + 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(' - ') +} diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.module.ts b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.module.ts new file mode 100644 index 0000000..bb77189 --- /dev/null +++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.module.ts @@ -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 +{ +} diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html index a642e32..6c9a2d9 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html @@ -48,68 +48,7 @@
-
-
-
- - - -
-
-
- {{deviceTitle(summary.value.device)}} -
- Last Updated on {{summary.value.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }} -
-
- -
-
-
-
Status
-
{{ deviceStatusString(summary.value.device.device_status) | titlecase}}
-
No Data
-
-
-
Temperature
-
{{ summary.value.smart?.temp }}°C
-
--
-
-
-
Capacity
-
{{ summary.value.device.capacity | fileSize}}
-
-
-
Powered On
-
{{ humanizeDuration(summary.value.smart?.power_on_hours * 60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] }) }}
-
--
-
-
-
-
+
diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts index a4ea071..d5d59a0 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts @@ -5,13 +5,12 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import {ApexOptions, ChartComponent} from 'ng-apexcharts'; import { DashboardService } from 'app/modules/dashboard/dashboard.service'; -import * as moment from 'moment'; import {MatDialog} from '@angular/material/dialog'; import { DashboardSettingsComponent } from 'app/layout/common/dashboard-settings/dashboard-settings.component'; -import humanizeDuration from 'humanize-duration' -import {AppConfig} from 'app/core/config/app.config'; -import { TreoConfigService } from '@treo/services/config'; -import {Router, NavigationEnd,ActivatedRoute} from '@angular/router'; +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({ selector : 'example', @@ -24,8 +23,8 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy { data: any; temperatureOptions: ApexOptions; - config: AppConfig; tempDurationKey: string = "forever" + config: AppConfig; // Private private _unsubscribeAll: Subject; @@ -38,11 +37,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy */ constructor( private _smartService: DashboardService, - public dialog: MatDialog, private _configService: TreoConfigService, + public dialog: MatDialog, private router: Router, - private activatedRoute: ActivatedRoute - ) { // Set the private defaults @@ -59,6 +56,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy */ ngOnInit(): void { + // Subscribe to config changes this._configService.config$ .pipe(takeUntil(this._unsubscribeAll)) @@ -202,49 +200,10 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy }; } - private _deviceDisplayTitle(disk, titleType: string){ - let deviceDisplay = '' - 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(' - ') - } - // ----------------------------------------------------------------------------------------------------- // @ Public methods // ----------------------------------------------------------------------------------------------------- - openDialog() { - const dialogRef = this.dialog.open(DashboardSettingsComponent); - - dialogRef.afterClosed().subscribe(result => { - console.log(`Dialog result: ${result}`); - }); - } - deviceTitle(disk){ console.log(`Displaying Device ${disk.wwn} with: ${this.config.dashboardDisplay}`) @@ -252,37 +211,17 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy if (disk.host_id) titleParts.push(disk.host_id) //add device identifier (fallback to generated device name) - titleParts.push(this._deviceDisplayTitle(disk, this.config.dashboardDisplay) || this._deviceDisplayTitle(disk, 'name')) + titleParts.push(deviceDisplayTitle(disk, this.config.dashboardDisplay) || deviceDisplayTitle(disk, 'name')) return titleParts.join(' - ') } - deviceStatusString(deviceStatus){ - if(deviceStatus == 0){ - return "passed" - } else { - return "failed" - } - } + openDialog() { + const dialogRef = this.dialog.open(DashboardSettingsComponent); - 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 '' - } + dialogRef.afterClosed().subscribe(result => { + console.log(`Dialog result: ${result}`); + }); } /* @@ -321,6 +260,4 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy return item.id || index; } - readonly humanizeDuration = humanizeDuration; - } diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.module.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.module.ts index 2de2203..0eca908 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.module.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.module.ts @@ -13,12 +13,13 @@ import { MatTableModule } from '@angular/material/table'; import { NgApexchartsModule } from 'ng-apexcharts'; import { MatTooltipModule } from '@angular/material/tooltip' import { DashboardSettingsModule } from "app/layout/common/dashboard-settings/dashboard-settings.module"; +import { DashboardDeviceModule } from "app/layout/common/dashboard-device/dashboard-device.module"; @NgModule({ declarations: [ DashboardComponent ], - imports : [ + imports: [ RouterModule.forChild(dashboardRoutes), MatButtonModule, MatDividerModule, @@ -30,7 +31,8 @@ import { DashboardSettingsModule } from "app/layout/common/dashboard-settings/da MatTableModule, NgApexchartsModule, SharedModule, - DashboardSettingsModule + DashboardSettingsModule, + DashboardDeviceModule ] }) export class DashboardModule From 83839f7faf699204dbc5a77ecdcf5bfb4a2dd9f1 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Fri, 20 May 2022 22:33:59 -0700 Subject: [PATCH 11/14] adding group by hostId support in dashboard. fixes #151 --- .../src/app/modules/dashboard/dashboard.component.html | 9 +++++++-- .../src/app/modules/dashboard/dashboard.component.ts | 10 ++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html index 6c9a2d9..cd53896 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html @@ -47,10 +47,15 @@ -
- + +
+

{{hostId.key}}

+
+ +
+
diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts index d5d59a0..0f09cec 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts @@ -22,6 +22,7 @@ import {Router} from "@angular/router"; export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy { data: any; + hostGroups: { [hostId: string]: string[] } = {} temperatureOptions: ApexOptions; tempDurationKey: string = "forever" config: AppConfig; @@ -86,6 +87,15 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy // Store the 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 this._prepareChartData(); }); From 9846ba13e02c8b8e1993448ffe7294cd68fde816 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 23 May 2022 08:19:58 -0700 Subject: [PATCH 12/14] adding support for device sort in UI. --- .../app/modules/dashboard/dashboard.component.html | 2 +- .../app/modules/dashboard/dashboard.component.ts | 8 ++++++++ webapp/frontend/src/app/shared/device-sort.pipe.ts | 14 +++++++------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html index cd53896..6fa33a6 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html @@ -51,7 +51,7 @@

{{hostId.key}}

- +
diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts index 0f09cec..bc45ea7 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts @@ -226,6 +226,14 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy return titleParts.join(' - ') } + deviceSummariesForHostGroup(hostGroupWWNs: string[]) { + let deviceSummaries = [] + for(let wwn of hostGroupWWNs){ + deviceSummaries.push(this.data.data.summary[wwn]) + } + return deviceSummaries + } + openDialog() { const dialogRef = this.dialog.open(DashboardSettingsComponent); diff --git a/webapp/frontend/src/app/shared/device-sort.pipe.ts b/webapp/frontend/src/app/shared/device-sort.pipe.ts index 6f115f1..57f2859 100644 --- a/webapp/frontend/src/app/shared/device-sort.pipe.ts +++ b/webapp/frontend/src/app/shared/device-sort.pipe.ts @@ -5,20 +5,20 @@ import { Pipe, PipeTransform } from '@angular/core'; }) export class DeviceSortPipe implements PipeTransform { - numericalStatus(device): number { - if(!device.smart_results[0]){ + numericalStatus(deviceSummary): number { + if(!deviceSummary.smart){ return 0 - } else if (device.smart_results[0].smart_status == 'passed'){ + } else if (deviceSummary.device.device_status == 0){ return 1 } else { - return -1 + return deviceSummary.device.device_status * -1 // will return range from -1, -2, -3 } } - transform(devices: Array, ...args: unknown[]): Array { + transform(deviceSummaries: Array, sortBy = ''): Array { //failed, unknown/empty, passed - devices.sort((a: any, b: any) => { + deviceSummaries.sort((a: any, b: any) => { let left = this.numericalStatus(a) let right = this.numericalStatus(b) @@ -27,7 +27,7 @@ export class DeviceSortPipe implements PipeTransform { }); - return devices; + return deviceSummaries; } } From 7979950c3bf038e32a6e88530b123e439a50b2fd Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 23 May 2022 08:49:51 -0700 Subject: [PATCH 13/14] fixing device sort and display title. fixes #194 --- .../dashboard-settings.component.html | 7 +-- .../dashboard/dashboard.component.html | 2 +- .../src/app/shared/device-sort.pipe.ts | 59 ++++++++++++++----- 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html index a90786b..39b82a3 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html +++ b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html @@ -4,7 +4,7 @@
- Display + Display Title Name Serial ID @@ -17,10 +17,7 @@ Sort By Status - Name - Serial ID - UUID - Label + Title
diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html index 6fa33a6..e719d09 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html @@ -51,7 +51,7 @@

{{hostId.key}}

- +
diff --git a/webapp/frontend/src/app/shared/device-sort.pipe.ts b/webapp/frontend/src/app/shared/device-sort.pipe.ts index 57f2859..bc07c70 100644 --- a/webapp/frontend/src/app/shared/device-sort.pipe.ts +++ b/webapp/frontend/src/app/shared/device-sort.pipe.ts @@ -1,31 +1,58 @@ import { Pipe, PipeTransform } from '@angular/core'; +import {deviceDisplayTitle} from "app/layout/common/dashboard-device/dashboard-device.component"; @Pipe({ name: 'deviceSort' }) export class DeviceSortPipe implements PipeTransform { - numericalStatus(deviceSummary): number { - if(!deviceSummary.smart){ - return 0 - } else if (deviceSummary.device.device_status == 0){ - return 1 - } else { - return deviceSummary.device.device_status * -1 // will return range from -1, -2, -3 + statusCompareFn(a: any, b: any) { + function deviceStatus(deviceSummary): number { + if(!deviceSummary.smart){ + return 0 + } else if (deviceSummary.device.device_status == 0){ + 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(deviceSummaries: Array, sortBy = ''): Array { + transform(deviceSummaries: Array, sortBy = 'status', dashboardDisplay = "name"): Array { + let compareFn = undefined + switch (sortBy) { + case 'status': + compareFn = this.statusCompareFn + break; + case 'title': + compareFn = this.titleCompareFn(dashboardDisplay) + break; + } + //failed, unknown/empty, passed - deviceSummaries.sort((a: any, b: any) => { - - let left = this.numericalStatus(a) - let right = this.numericalStatus(b) - - return left - right; - }); - + deviceSummaries.sort(compareFn); return deviceSummaries; } From 8c07e91f39e7c1d1a377f385261dc2a9b9bdd411 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 23 May 2022 09:23:22 -0700 Subject: [PATCH 14/14] grey out and mark thresholds as not yet implemented. --- .../dashboard-settings.component.html | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html index 39b82a3..b920e63 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html +++ b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html @@ -26,52 +26,52 @@ -
+
Critical Error Threshold - + Critical Warning Threshold - +
-
+
Error Threshold - + Warning Threshold - +
-
+
Critical Error Threshold - + Critical Warning Threshold - +
-
+
Critical Error Threshold - + Critical Warning Threshold - +