From 600cd153e0d0fb9aef9c723fffdc2f28a76b5a1f Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 21 Feb 2025 09:23:48 +0100 Subject: [PATCH 1/2] 491 [FEAT] Allow disks to be hidden/archived - Add archived to device type & db - Add archive/unarchive handlers to webapp backend - Add archive toggle & styling to webapp frontend --- docs/dbdiagram.io.txt | 1 + webapp/backend/pkg/database/interface.go | 1 + .../migrations/m20220509170100/device.go | 8 +-- .../migrations/m20250221084400/device.go | 41 ++++++++++++ .../pkg/database/mock/mock_database.go | 14 ++++ .../database/scrutiny_repository_device.go | 14 +++- .../scrutiny_repository_migrations.go | 10 +++ webapp/backend/pkg/models/device.go | 1 + .../backend/pkg/web/handler/archive_device.go | 22 +++++++ .../pkg/web/handler/unarchive_device.go | 22 +++++++ webapp/backend/pkg/web/server.go | 6 +- .../src/app/core/models/device-model.ts | 1 + ...board-device-archive-dialog.component.html | 11 ++++ ...board-device-archive-dialog.component.scss | 0 ...rd-device-archive-dialog.component.spec.ts | 64 +++++++++++++++++++ ...shboard-device-archive-dialog.component.ts | 29 +++++++++ .../dashboard-device-archive-dialog.module.ts | 29 +++++++++ ...dashboard-device-archive-dialog.service.ts | 38 +++++++++++ .../dashboard-device.component.html | 16 ++++- .../dashboard-device.component.scss | 3 + .../dashboard-device.component.ts | 32 +++++++++- .../dashboard-device.module.ts | 4 +- .../dashboard/dashboard.component.html | 39 +++++++---- .../modules/dashboard/dashboard.component.ts | 9 +++ 24 files changed, 390 insertions(+), 25 deletions(-) create mode 100644 webapp/backend/pkg/database/migrations/m20250221084400/device.go create mode 100644 webapp/backend/pkg/web/handler/archive_device.go create mode 100644 webapp/backend/pkg/web/handler/unarchive_device.go create mode 100644 webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.html create mode 100644 webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.scss create mode 100644 webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.spec.ts create mode 100644 webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.ts create mode 100644 webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.module.ts create mode 100644 webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.service.ts diff --git a/docs/dbdiagram.io.txt b/docs/dbdiagram.io.txt index 23265ad..ce1dd83 100644 --- a/docs/dbdiagram.io.txt +++ b/docs/dbdiagram.io.txt @@ -2,6 +2,7 @@ // SQLite Table(s) Table Device { + Archived bool //GORM attributes, see: http://gorm.io/docs/conventions.html CreatedAt time UpdatedAt time diff --git a/webapp/backend/pkg/database/interface.go b/webapp/backend/pkg/database/interface.go index 8613eae..ee7066e 100644 --- a/webapp/backend/pkg/database/interface.go +++ b/webapp/backend/pkg/database/interface.go @@ -20,6 +20,7 @@ type DeviceRepo interface { UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error) UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) GetDeviceDetails(ctx context.Context, wwn string) (models.Device, error) + UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error DeleteDevice(ctx context.Context, wwn string) error SaveSmartAttributes(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (measurements.Smart, error) diff --git a/webapp/backend/pkg/database/migrations/m20220509170100/device.go b/webapp/backend/pkg/database/migrations/m20220509170100/device.go index 1134fff..c8be222 100644 --- a/webapp/backend/pkg/database/migrations/m20220509170100/device.go +++ b/webapp/backend/pkg/database/migrations/m20220509170100/device.go @@ -5,6 +5,7 @@ import ( "time" ) +// Deprecated: m20220509170100.Device is deprecated, only used by db migrations type Device struct { //GORM attributes, see: http://gorm.io/docs/conventions.html CreatedAt time.Time @@ -14,9 +15,9 @@ 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"` + 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"` @@ -38,4 +39,3 @@ type Device struct { // Data set by Scrutiny DeviceStatus pkg.DeviceStatus `json:"device_status"` } - diff --git a/webapp/backend/pkg/database/migrations/m20250221084400/device.go b/webapp/backend/pkg/database/migrations/m20250221084400/device.go new file mode 100644 index 0000000..b0a2e5a --- /dev/null +++ b/webapp/backend/pkg/database/migrations/m20250221084400/device.go @@ -0,0 +1,41 @@ +package m20250221084400 + +import ( + "github.com/analogj/scrutiny/webapp/backend/pkg" + "time" +) + +type Device struct { + Archived bool `json:"archived"` + //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/mock/mock_database.go b/webapp/backend/pkg/database/mock/mock_database.go index c001d08..f5fefc2 100644 --- a/webapp/backend/pkg/database/mock/mock_database.go +++ b/webapp/backend/pkg/database/mock/mock_database.go @@ -52,6 +52,20 @@ func (mr *MockDeviceRepoMockRecorder) Close() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockDeviceRepo)(nil).Close)) } +// UpdateDeviceArchived mocks base method. +func (m *MockDeviceRepo) UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateDeviceArchived", ctx, wwn) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateDeviceArchived indicates an expected call of UpdateDeviceArchived. +func (mr *MockDeviceRepoMockRecorder) UpdateDeviceArchived(ctx, wwn, archived interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeviceArchived", reflect.TypeOf((*MockDeviceRepo)(nil).UpdateDeviceArchived), ctx, wwn, archived) +} + // DeleteDevice mocks base method. func (m *MockDeviceRepo) DeleteDevice(ctx context.Context, wwn string) error { m.ctrl.T.Helper() diff --git a/webapp/backend/pkg/database/scrutiny_repository_device.go b/webapp/backend/pkg/database/scrutiny_repository_device.go index 897a3e1..8ba0e8c 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_device.go +++ b/webapp/backend/pkg/database/scrutiny_repository_device.go @@ -14,7 +14,7 @@ import ( // Device //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -//insert device into DB (and update specified columns if device is already registered) +// insert device into DB (and update specified columns if device is already registered) // update device fields that may change: (DeviceType, HostID) func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev models.Device) error { if err := sr.gormClient.WithContext(ctx).Clauses(clause.OnConflict{ @@ -51,7 +51,7 @@ func (sr *scrutinyRepository) UpdateDevice(ctx context.Context, wwn string, coll return device, sr.gormClient.Model(&device).Updates(device).Error } -//Update Device Status +// Update Device Status func (sr *scrutinyRepository) UpdateDeviceStatus(ctx context.Context, wwn string, status pkg.DeviceStatus) (models.Device, error) { var device models.Device if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil { @@ -74,6 +74,16 @@ func (sr *scrutinyRepository) GetDeviceDetails(ctx context.Context, wwn string) return device, nil } +// Update Device Archived State +func (sr *scrutinyRepository) UpdateDeviceArchived(ctx context.Context, wwn string, archived bool) error { + var device models.Device + if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).First(&device).Error; err != nil { + return fmt.Errorf("Could not get device from DB: %v", err) + } + + return sr.gormClient.Model(&device).Where("wwn = ?", wwn).Update("archived", archived).Error +} + func (sr *scrutinyRepository) DeleteDevice(ctx context.Context, wwn string) error { if err := sr.gormClient.WithContext(ctx).Where("wwn = ?", wwn).Delete(&models.Device{}).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 9a02574..e12fae8 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_migrations.go +++ b/webapp/backend/pkg/database/scrutiny_repository_migrations.go @@ -12,6 +12,7 @@ import ( "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/database/migrations/m20220716214900" + "github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20250221084400" "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" @@ -399,6 +400,15 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error { return tx.Create(&defaultSettings).Error }, }, + { + ID: "m20250221084400", // add archived to device data + Migrate: func(tx *gorm.DB) error { + + //migrate the device database. + // adding column (archived) + return tx.AutoMigrate(m20250221084400.Device{}) + }, + }, }) if err := m.Migrate(); err != nil { diff --git a/webapp/backend/pkg/models/device.go b/webapp/backend/pkg/models/device.go index 9c84115..a891652 100644 --- a/webapp/backend/pkg/models/device.go +++ b/webapp/backend/pkg/models/device.go @@ -14,6 +14,7 @@ type DeviceWrapper struct { type Device struct { //GORM attributes, see: http://gorm.io/docs/conventions.html + Archived bool `json:"archived"` CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time diff --git a/webapp/backend/pkg/web/handler/archive_device.go b/webapp/backend/pkg/web/handler/archive_device.go new file mode 100644 index 0000000..494dd38 --- /dev/null +++ b/webapp/backend/pkg/web/handler/archive_device.go @@ -0,0 +1,22 @@ +package handler + +import ( + "github.com/analogj/scrutiny/webapp/backend/pkg/database" + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "net/http" +) + +func ArchiveDevice(c *gin.Context) { + logger := c.MustGet("LOGGER").(*logrus.Entry) + deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) + + err := deviceRepo.UpdateDeviceArchived(c, c.Param("wwn"), true) + if err != nil { + logger.Errorln("An error occurred while archiving device", err) + c.JSON(http.StatusInternalServerError, gin.H{"success": false}) + return + } + + c.JSON(http.StatusOK, gin.H{"success": true}) +} diff --git a/webapp/backend/pkg/web/handler/unarchive_device.go b/webapp/backend/pkg/web/handler/unarchive_device.go new file mode 100644 index 0000000..dea8781 --- /dev/null +++ b/webapp/backend/pkg/web/handler/unarchive_device.go @@ -0,0 +1,22 @@ +package handler + +import ( + "github.com/analogj/scrutiny/webapp/backend/pkg/database" + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "net/http" +) + +func UnarchiveDevice(c *gin.Context) { + logger := c.MustGet("LOGGER").(*logrus.Entry) + deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo) + + err := deviceRepo.UpdateDeviceArchived(c, c.Param("wwn"), false) + if err != nil { + logger.Errorln("An error occurred while unarchiving device", err) + c.JSON(http.StatusInternalServerError, gin.H{"success": false}) + return + } + + c.JSON(http.StatusOK, gin.H{"success": true}) +} diff --git a/webapp/backend/pkg/web/server.go b/webapp/backend/pkg/web/server.go index 3d56fe0..38383c9 100644 --- a/webapp/backend/pkg/web/server.go +++ b/webapp/backend/pkg/web/server.go @@ -42,8 +42,10 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine { api.GET("/summary/temp", handler.GetDevicesSummaryTempHistory) //used by Dashboard (Temperature history dropdown) api.POST("/device/:wwn/smart", handler.UploadDeviceMetrics) //used by Collector to upload data api.POST("/device/:wwn/selftest", handler.UploadDeviceSelfTests) - api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details - api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device + api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details + api.POST("/device/:wwn/archive", handler.ArchiveDevice) //used by UI to archive device + api.POST("/device/:wwn/unarchive", handler.UnarchiveDevice) //used by UI to unarchive device + api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device api.GET("/settings", handler.GetSettings) //used to get settings api.POST("/settings", handler.SaveSettings) //used to save settings diff --git a/webapp/frontend/src/app/core/models/device-model.ts b/webapp/frontend/src/app/core/models/device-model.ts index 7613c3f..c07d61a 100644 --- a/webapp/frontend/src/app/core/models/device-model.ts +++ b/webapp/frontend/src/app/core/models/device-model.ts @@ -1,5 +1,6 @@ // maps to webapp/backend/pkg/models/device.go export interface DeviceModel { + archived: boolean; wwn: string; device_name?: string; device_uuid?: string; diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.html b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.html new file mode 100644 index 0000000..aa8d88e --- /dev/null +++ b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.html @@ -0,0 +1,11 @@ +

Archive {{data.title}}?

+This will remove the device from Scrutiny dashboard, unless you toggle show archived. Any data about the device + itself will remain untouched. + + + + diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.scss b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.spec.ts b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.spec.ts new file mode 100644 index 0000000..3cffb95 --- /dev/null +++ b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.spec.ts @@ -0,0 +1,64 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; + +import {DashboardDeviceArchiveDialogComponent} from './dashboard-device-archive-dialog.component'; +import {HttpClientModule} from '@angular/common/http'; +import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog'; +import {MatButtonModule} from '@angular/material/button'; +import {MatIconModule} from '@angular/material/icon'; +import {SharedModule} from '../../../shared/shared.module'; +import {DashboardDeviceArchiveDialogService} from './dashboard-device-archive-dialog.service'; +import {of} from 'rxjs'; + + +describe('DashboardDeviceArchiveDialogComponent', () => { + let component: DashboardDeviceArchiveDialogComponent; + let fixture: ComponentFixture; + + const matDialogRefSpy = jasmine.createSpyObj('MatDialogRef', ['closeDialog', 'close']); + const dashboardDeviceArchiveDialogServiceSpy = jasmine.createSpyObj('DashboardDeviceArchiveDialogService', ['archiveDevice']); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientModule, + MatDialogModule, + MatButtonModule, + MatIconModule, + SharedModule, + ], + providers: [ + {provide: MatDialogRef, useValue: matDialogRefSpy}, + {provide: MAT_DIALOG_DATA, useValue: {wwn: 'test-wwn', title: 'my-test-device-title'}}, + {provide: DashboardDeviceArchiveDialogService, useValue: dashboardDeviceArchiveDialogServiceSpy} + ], + declarations: [DashboardDeviceArchiveDialogComponent] + }) + .compileComponents() + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DashboardDeviceArchiveDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should close the component if cancel is clicked', () => { + matDialogRefSpy.closeDialog.calls.reset(); + matDialogRefSpy.closeDialog() + expect(matDialogRefSpy.closeDialog).toHaveBeenCalled(); + }); + + it('should attempt to archive device if archive is clicked', () => { + dashboardDeviceArchiveDialogServiceSpy.archiveDevice.and.returnValue(of({'success': true})); + + component.onArchiveClick() + expect(dashboardDeviceArchiveDialogServiceSpy.archiveDevice).toHaveBeenCalledWith('test-wwn'); + expect(dashboardDeviceArchiveDialogServiceSpy.archiveDevice.calls.count()) + .withContext('one call') + .toBe(1); + }); +}); diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.ts b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.ts new file mode 100644 index 0000000..979aeb8 --- /dev/null +++ b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.component.ts @@ -0,0 +1,29 @@ +import {Component, Inject, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {DashboardDeviceArchiveDialogService} from 'app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.service'; + +@Component({ + selector: 'app-dashboard-device-archive-dialog', + templateUrl: './dashboard-device-archive-dialog.component.html', + styleUrls: ['./dashboard-device-archive-dialog.component.scss'], +}) +export class DashboardDeviceArchiveDialogComponent implements OnInit { + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: {wwn: string, title: string}, + private _archiveService: DashboardDeviceArchiveDialogService, + ) { + } + + ngOnInit(): void { + } + + onArchiveClick(): void { + this._archiveService.archiveDevice(this.data.wwn) + .subscribe((data) => { + this.dialogRef.close(data); + }); + + } +} diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.module.ts b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.module.ts new file mode 100644 index 0000000..44a638f --- /dev/null +++ b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.module.ts @@ -0,0 +1,29 @@ +import {NgModule} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {MatButtonModule} from '@angular/material/button'; +import {MatIconModule} from '@angular/material/icon'; +import {SharedModule} from 'app/shared/shared.module'; +import {dashboardRoutes} from 'app/modules/dashboard/dashboard.routing'; +import {MatDialogModule} from '@angular/material/dialog'; +import {DashboardDeviceArchiveDialogComponent} from './dashboard-device-archive-dialog.component'; + +@NgModule({ + declarations: [ + DashboardDeviceArchiveDialogComponent + ], + imports: [ + RouterModule.forChild([]), + RouterModule.forChild(dashboardRoutes), + MatButtonModule, + MatIconModule, + SharedModule, + MatDialogModule + ], + exports : [ + DashboardDeviceArchiveDialogComponent, + ], + providers : [] +}) +export class DashboardDeviceArchiveDialogModule +{ +} diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.service.ts b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.service.ts new file mode 100644 index 0000000..a8c1083 --- /dev/null +++ b/webapp/frontend/src/app/layout/common/dashboard-device-archive-dialog/dashboard-device-archive-dialog.service.ts @@ -0,0 +1,38 @@ +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {Observable} from 'rxjs'; +import {getBasePath} from 'app/app.routing'; + +@Injectable({ + providedIn: 'root' +}) +export class DashboardDeviceArchiveDialogService +{ + + + /** + * Constructor + * + * @param {HttpClient} _httpClient + */ + constructor( + private _httpClient: HttpClient + ) + { + } + + // ----------------------------------------------------------------------------------------------------- + // @ Public methods + // ----------------------------------------------------------------------------------------------------- + + + archiveDevice(wwn: string): Observable + { + return this._httpClient.post( `${getBasePath()}/api/device/${wwn}/archive`, {}); + } + + unarchiveDevice(wwn: string): Observable + { + return this._httpClient.post( `${getBasePath()}/api/device/${wwn}/unarchive`, {}); + } +} 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 index 43f4e0b..01ffb5f 100644 --- 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 @@ -1,5 +1,7 @@ -
+ + +
@@ -67,13 +83,13 @@
Temperature
-
Temperature history for each device
+
Temperature history for each device
@@ -109,7 +125,8 @@ src="assets/images/dashboard-placeholder.png">

No Devices Detected!

-

Scrutiny includes a Collector agent that you must run on all of your systems. The Collector is responsible for detecting connected storage devices and collecting S.M.A.R.T data on a configurable schedule.

+

Scrutiny includes a Collector agent that you must run on all of your systems. The Collector is responsible for detecting connected storage + devices and collecting S.M.A.R.T data on a configurable schedule.


You can trigger the Collector manually by running the following command, then refreshing this page:

scrutiny-collector-metrics run diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts index 5c7d9c8..d0e28da 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts @@ -34,6 +34,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy temperatureOptions: ApexOptions; tempDurationKey = 'forever' config: AppConfig; + showArchived: boolean; // Private private _unsubscribeAll: Subject; @@ -248,6 +249,14 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy delete this.summaryData[wwn] // remove the device from the summary list. } + onDeviceArchived(wwn: string): void { + this.summaryData[wwn].device.archived = true; + } + + onDeviceUnarchived(wwn: string): void { + this.summaryData[wwn].device.archived = false; + } + /* DURATION_KEY_WEEK = "week" From 3e115832833cb4e9915afd89568178928e01171c Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 28 Apr 2025 15:01:07 +0200 Subject: [PATCH 2/2] 491 [FEAT] Allow disks to be hidden/archived - Fix mock device type definition mismatch in the frontend. - Make DeviceModel archived field optional. --- .../src/app/core/models/device-model.ts | 2 +- .../src/app/data/mock/device/details/sda.ts | 3 ++- .../src/app/data/mock/summary/data.ts | 21 ++++++++++++------- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/webapp/frontend/src/app/core/models/device-model.ts b/webapp/frontend/src/app/core/models/device-model.ts index c07d61a..bddb776 100644 --- a/webapp/frontend/src/app/core/models/device-model.ts +++ b/webapp/frontend/src/app/core/models/device-model.ts @@ -1,6 +1,6 @@ // maps to webapp/backend/pkg/models/device.go export interface DeviceModel { - archived: boolean; + archived?: boolean; wwn: string; device_name?: string; device_uuid?: string; diff --git a/webapp/frontend/src/app/data/mock/device/details/sda.ts b/webapp/frontend/src/app/data/mock/device/details/sda.ts index 6406e5a..b0b0d4d 100644 --- a/webapp/frontend/src/app/data/mock/device/details/sda.ts +++ b/webapp/frontend/src/app/data/mock/device/details/sda.ts @@ -20,7 +20,8 @@ export const sda = { 'device_type': '', 'label': '', 'host_id': '', - 'device_status': 0 + 'device_status': 0, + 'archived': false }, 'smart_results': [{ 'date': '2021-10-24T23:20:44Z', diff --git a/webapp/frontend/src/app/data/mock/summary/data.ts b/webapp/frontend/src/app/data/mock/summary/data.ts index 8530a8d..5156f9e 100644 --- a/webapp/frontend/src/app/data/mock/summary/data.ts +++ b/webapp/frontend/src/app/data/mock/summary/data.ts @@ -28,7 +28,8 @@ export const summary = { 'device_type': '', 'label': '', 'host_id': '', - 'device_status': 0 + 'device_status': 0, + 'archived': false } }, '0x5000cca252c859cc': { @@ -55,7 +56,8 @@ export const summary = { 'device_type': '', 'label': '', 'host_id': '', - 'device_status': 0 + 'device_status': 0, + 'archived': false }, 'smart': { 'collector_date': '2020-08-21T22:27:02Z', @@ -91,7 +93,8 @@ export const summary = { 'device_type': '', 'label': '', 'host_id': '', - 'device_status': 0 + 'device_status': 0, + 'archived': false }, 'smart': { 'collector_date': '2020-06-21T00:03:30Z', @@ -127,7 +130,8 @@ export const summary = { 'device_type': '', 'label': '', 'host_id': '', - 'device_status': 0 + 'device_status': 0, + 'archived': false } }, '0x5000cca264ec3183': { @@ -154,7 +158,8 @@ export const summary = { 'device_type': '', 'label': '', 'host_id': 'custom host id', - 'device_status': 1 + 'device_status': 1, + 'archived': false }, 'smart': { 'collector_date': '2020-09-13T16:29:23Z', @@ -574,7 +579,8 @@ export const summary = { 'device_type': '', 'label': '', 'host_id': '', - 'device_status': 0 + 'device_status': 0, + 'archived': false } }, '0x5002538e40a22954': { @@ -601,7 +607,8 @@ export const summary = { 'device_type': '', 'label': '', 'host_id': '', - 'device_status': 0 + 'device_status': 0, + 'archived': false }, 'smart': { 'collector_date': '2020-06-10T12:01:02Z',