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/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"` +} + 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 a7266d9..0d0bde9 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_migrations.go +++ b/webapp/backend/pkg/database/scrutiny_repository_migrations.go @@ -6,6 +6,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" @@ -253,10 +254,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"` 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/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 70acf16..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", @@ -138,7 +153,7 @@ export const summary = { "device_protocol": "", "device_type": "", "label": "", - "host_id": "", + "host_id": "custom host id", "device_status": 1 }, "smart": { @@ -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-device/dashboard-device.component.html b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html new file mode 100644 index 0000000..df68121 --- /dev/null +++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.html @@ -0,0 +1,61 @@ +
+
+ + + +
+
+
+ {{deviceTitle(deviceSummary.device)}} +
+ Last Updated on {{deviceSummary.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }} +
+
+ +
+
+
+
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/layout/common/dashboard-settings/dashboard-settings.component.html b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.html index dddab9b..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 @@ -1,14 +1,23 @@

Scrutiny Settings

-
-
- +
+
+ + Display Title + + Name + Serial ID + UUID + Label + + + + Sort By - - Status - Name - Label + + Status + Title
@@ -17,61 +26,61 @@ -
+
Critical Error Threshold - + Critical Warning Threshold - +
-
+
Error Threshold - + Warning Threshold - +
-
+
Critical Error Threshold - + Critical Warning Threshold - +
-
+
Critical Error Threshold - + Critical Warning Threshold - +
- +
- + 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..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 @@ -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,41 @@ 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 { + var newSettings = { + dashboardDisplay: this.dashboardDisplay, + dashboardSort: this.dashboardSort + } + this._configService.config = newSettings + console.log(`Saved Settings: ${JSON.stringify(newSettings)}`) } + formatLabel(value: number) { return value; } diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html index 8e321c4..e719d09 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html @@ -47,71 +47,15 @@
-
-
-
-
- - - -
-
-
- {{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'] }) }}
-
--
-
-
-
+ +
+

{{hostId.key}}

+
+
+
@@ -123,22 +67,22 @@
- - - + + + +
- ; + @ViewChild("tempChart", { static: false }) tempChart: ChartComponent; /** * Constructor @@ -32,7 +38,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy */ constructor( private _smartService: DashboardService, - public dialog: MatDialog + private _configService: TreoConfigService, + public dialog: MatDialog, + private router: Router, ) { // Set the private defaults @@ -49,6 +57,28 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy */ 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 this._smartService.data$ .pipe(takeUntil(this._unsubscribeAll)) @@ -57,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(); }); @@ -81,6 +120,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 = [] @@ -91,8 +138,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy if (!deviceSummary.temp_history){ continue } + + let deviceName = this.deviceTitle(deviceSummary.device) + var deviceSeriesMetadata = { - name: `/dev/${deviceSummary.device.device_name}`, + name: deviceName, data: [] } @@ -164,6 +214,26 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy // @ 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() { 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) - - title.push(`/dev/${disk.device_name}`) - - if (disk.device_type && disk.device_type != 'scsi' && disk.device_type != 'ata'){ - title.push(disk.device_type) - } + /* - title.push(disk.model_name) + DURATION_KEY_WEEK = "week" + DURATION_KEY_MONTH = "month" + DURATION_KEY_YEAR = "year" + DURATION_KEY_FOREVER = "forever" + */ - return title.join(' - ') - } + changeSummaryTempDuration(durationKey: string){ + this.tempDurationKey = durationKey - deviceStatusString(deviceStatus){ - if(deviceStatus == 0){ - return "passed" - } else { - return "failed" - } - } + this._smartService.getSummaryTempData(durationKey) + .subscribe((data) => { - 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' - } + // 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] || [] + } - } else { - return '' - } + // Prepare the chart series data + this.tempChart.updateSeries(this._deviceDataTemperatureSeries()) + }); } /** @@ -227,6 +278,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 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}); + } } diff --git a/webapp/frontend/src/app/shared/device-sort.pipe.ts b/webapp/frontend/src/app/shared/device-sort.pipe.ts index 6f115f1..bc07c70 100644 --- a/webapp/frontend/src/app/shared/device-sort.pipe.ts +++ b/webapp/frontend/src/app/shared/device-sort.pipe.ts @@ -1,33 +1,60 @@ 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(device): number { - if(!device.smart_results[0]){ - return 0 - } else if (device.smart_results[0].smart_status == 'passed'){ - return 1 - } else { - return -1 + 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') - transform(devices: Array, ...args: unknown[]): Array { - //failed, unknown/empty, passed - devices.sort((a: any, b: any) => { + if( left < right ) + return -1; - let left = this.numericalStatus(a) - let right = this.numericalStatus(b) + if( left > right ) + return 1; - return left - right; - }); + return 0; + } + } - return devices; + 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(compareFn); + + return deviceSummaries; } }