Merge pull request #754 from Berry-95/491-FEAT-Allow-disks-to-be-archived

Fixes 491 [FEAT] Allow disks to be hidden/archived
This commit is contained in:
Jason Kulatunga
2025-04-30 10:31:21 -04:00
committed by GitHub
26 changed files with 406 additions and 33 deletions
+1
View File
@@ -2,6 +2,7 @@
// SQLite Table(s)
Table Device {
Archived bool
//GORM attributes, see: http://gorm.io/docs/conventions.html
CreatedAt time
UpdatedAt time
+1
View File
@@ -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)
@@ -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
@@ -38,4 +39,3 @@ type Device struct {
// Data set by Scrutiny
DeviceStatus pkg.DeviceStatus `json:"device_status"`
}
@@ -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"`
}
@@ -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()
@@ -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
@@ -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 {
+1
View File
@@ -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
@@ -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})
}
@@ -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})
}
+2
View File
@@ -43,6 +43,8 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
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.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
@@ -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;
@@ -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',
@@ -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',
@@ -0,0 +1,11 @@
<h2 mat-dialog-title>Archive {{data.title}}?</h2>
<mat-dialog-content>This will remove the device from Scrutiny dashboard, unless you toggle show archived. <strong>Any data about the device
itself will remain untouched.</strong></mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close>Cancel</button>
<button class="yellow-600" mat-button (click)="onArchiveClick()">
<mat-icon class="icon-size-20 mr-3"
[svgIcon]="'archive'"></mat-icon>
Archive
</button>
</mat-dialog-actions>
@@ -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<DashboardDeviceArchiveDialogComponent>;
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);
});
});
@@ -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<DashboardDeviceArchiveDialogComponent>,
@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);
});
}
}
@@ -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
{
}
@@ -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<any>
{
return this._httpClient.post( `${getBasePath()}/api/device/${wwn}/archive`, {});
}
unarchiveDevice(wwn: string): Observable<any>
{
return this._httpClient.post( `${getBasePath()}/api/device/${wwn}/unarchive`, {});
}
}
@@ -1,5 +1,7 @@
<div [ngClass]="{ 'border-green': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'passed',
'border-red': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'failed' }"
<div
[ngClass]="{ 'border-green': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'passed',
'border-red': deviceStatusForModelWithThreshold(deviceSummary.device, !!deviceSummary.smart, config.metrics.status_threshold) == 'failed',
'text-disabled': deviceSummary.device.archived }"
class="relative flex flex-col flex-auto p-6 pr-3 pb-3 bg-card rounded border-l-4 shadow-md overflow-hidden">
<div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
<mat-icon class="icon-size-96 opacity-12 text-green"
@@ -21,6 +23,8 @@
</div>
</div>
<div class="ml-auto" *ngIf="deviceSummary.device">
<mat-icon *ngIf="deviceSummary.device.archived"
[svgIcon]="'archive'"></mat-icon>
<button mat-icon-button
[matMenuTriggerFor]="previousStatementMenu">
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
@@ -33,6 +37,14 @@
<span>View Details</span>
</span>
</a>
<a mat-menu-item
(click)="openArchiveDialog()">
<span class="flex items-center">
<mat-icon class="icon-size-20 mr-3"
[svgIcon]="deviceSummary.device.archived ? 'unarchive':'archive'"></mat-icon>
<span>{{deviceSummary.device.archived ? "Unarchive" : "Archive"}} Device</span>
</span>
</a>
<a mat-menu-item (click)="openDeleteDialog()">
<span class="flex items-center">
<mat-icon class="icon-size-20 mr-3"
@@ -0,0 +1,3 @@
.text-disabled{
opacity: 0.8;
}
@@ -9,6 +9,8 @@ import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
import {DeviceStatusPipe} from 'app/shared/device-status.pipe';
import {DashboardDeviceArchiveDialogComponent} from '../dashboard-device-archive-dialog/dashboard-device-archive-dialog.component';
import {DashboardDeviceArchiveDialogService} from '../dashboard-device-archive-dialog/dashboard-device-archive-dialog.service';
@Component({
selector: 'app-dashboard-device',
@@ -19,6 +21,7 @@ export class DashboardDeviceComponent implements OnInit {
constructor(
private _configService: ScrutinyConfigService,
private _archiveService: DashboardDeviceArchiveDialogService,
public dialog: MatDialog,
) {
// Set the private defaults
@@ -26,7 +29,8 @@ export class DashboardDeviceComponent implements OnInit {
}
@Input() deviceSummary: DeviceSummaryModel;
@Input() deviceWWN: string;
@Output() deviceArchived = new EventEmitter<string>();
@Output() deviceUnarchived = new EventEmitter<string>();
@Output() deviceDeleted = new EventEmitter<string>();
config: AppConfig;
@@ -69,11 +73,33 @@ export class DashboardDeviceComponent implements OnInit {
}
}
openArchiveDialog(): void {
if(this.deviceSummary.device.archived){
this._archiveService.unarchiveDevice(this.deviceSummary.device.wwn).subscribe((result) => {
if(result) {
this.deviceUnarchived.emit(this.deviceSummary.device.wwn)
}
})
return;
}
const dialogRef = this.dialog.open(DashboardDeviceArchiveDialogComponent, {
data: {
wwn: this.deviceSummary.device.wwn,
title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboard_display)
}
});
dialogRef.afterClosed().subscribe(result => {
if(result) {
this.deviceArchived.emit(this.deviceSummary.device.wwn);
}
})
}
openDeleteDialog(): void {
const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, {
// width: '250px',
data: {
wwn: this.deviceWWN,
wwn: this.deviceSummary.device.wwn,
title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboard_display)
}
});
@@ -81,7 +107,7 @@ export class DashboardDeviceComponent implements OnInit {
dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed', result);
if (result.success) {
this.deviceDeleted.emit(this.deviceWWN)
this.deviceDeleted.emit(this.deviceSummary.device.wwn)
}
});
}
@@ -7,6 +7,7 @@ import {DashboardDeviceComponent} from 'app/layout/common/dashboard-device/dashb
import {dashboardRoutes} from '../../../modules/dashboard/dashboard.routing';
import {MatMenuModule} from '@angular/material/menu';
import {DashboardDeviceDeleteDialogModule} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module';
import {DashboardDeviceArchiveDialogModule} from '../dashboard-device-archive-dialog/dashboard-device-archive-dialog.module';
@NgModule({
declarations: [
@@ -19,7 +20,8 @@ import {DashboardDeviceDeleteDialogModule} from 'app/layout/common/dashboard-dev
MatIconModule,
MatMenuModule,
SharedModule,
DashboardDeviceDeleteDialogModule
DashboardDeviceDeleteDialogModule,
DashboardDeviceArchiveDialogModule
],
exports: [
DashboardDeviceComponent,
@@ -1,4 +1,3 @@
<div *ngIf="summaryData; else emptyDashboard">
<div class="flex flex-col flex-auto w-full p-8 xs:p-2">
@@ -11,7 +10,15 @@
</div>
<!-- Action buttons -->
<div class="flex items-center">
<button matTooltip="not yet implemented" class="xs:hidden" mat-stroked-button>
<button class="xs:hidden" mat-stroked-button
[color]="showArchived ? 'primary' : null"
(click)="showArchived=!showArchived">
<mat-icon class="icon-size-20"
[color]="showArchived ? 'primary' : null"
[svgIcon]="'archive'"></mat-icon>
<span class="ml-2">Archived</span>
</button>
<button matTooltip="not yet implemented" class="ml-2 xs:hidden" mat-stroked-button>
<mat-icon class="icon-size-20"
[svgIcon]="'save'"></mat-icon>
<span class="ml-2">Export</span>
@@ -31,6 +38,12 @@
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
</button>
<mat-menu #actionsMenu="matMenu">
<button mat-menu-item (click)="showArchived=!showArchived">
<mat-icon class="icon-size-20"
[color]="showArchived ? 'primary' : null"
[svgIcon]="'archive'"></mat-icon>
<span class="ml-2">Archived</span>
</button>
<button mat-menu-item
matTooltip="not yet implemented">
<mat-icon class="icon-size-20"
@@ -49,13 +62,16 @@
<div class="flex flex-wrap w-full" *ngFor="let hostId of hostGroups | keyvalue">
<h3 class="ml-4" *ngIf="hostId.key">{{hostId.key}}</h3>
<h3 class="ml-4" *ngIf="hostId.key">{{ hostId.key }}</h3>
<div class="flex flex-wrap w-full">
<app-dashboard-device (deviceDeleted)="onDeviceDeleted($event)"
<ng-container *ngFor="let deviceSummary of (deviceSummariesForHostGroup(hostId.value) | deviceSort:config.dashboard_sort:config.dashboard_display )">
<app-dashboard-device *ngIf="showArchived || !deviceSummary.device.archived"
(deviceArchived)="onDeviceArchived($event)"
(deviceUnarchived)="onDeviceUnarchived($event)"
(deviceDeleted)="onDeviceDeleted($event)"
class="flex gt-sm:w-1/2 min-w-80 p-4"
*ngFor="let deviceSummary of (deviceSummariesForHostGroup(hostId.value) | deviceSort:config.dashboard_sort:config.dashboard_display )"
[deviceWWN]="deviceSummary.device.wwn"
[deviceSummary]="deviceSummary"></app-dashboard-device>
</ng-container>
</div>
</div>
@@ -67,13 +83,13 @@
<div class="flex items-center justify-between">
<div class="flex flex-col">
<div class="font-bold text-md text-secondary uppercase tracking-wider mr-4">Temperature</div>
<div class="text-sm text-hint font-medium">Temperature history for each device </div>
<div class="text-sm text-hint font-medium">Temperature history for each device</div>
</div>
<div>
<button class="h-8 min-h-8 px-2"
mat-button
[matMenuTriggerFor]="tempRangeMenu">
<span class="font-medium text-sm text-hint">{{tempDurationKey}}</span>
<span class="font-medium text-sm text-hint">{{ tempDurationKey }}</span>
</button>
<mat-menu #tempRangeMenu="matMenu">
<button (click)="changeSummaryTempDuration('forever')" mat-menu-item>forever</button>
@@ -109,7 +125,8 @@
src="assets/images/dashboard-placeholder.png">
<h1>No Devices Detected!</h1>
<p style="max-width:700px;">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.</p>
<p style="max-width:700px;">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.</p>
<p><br/>You can trigger the Collector manually by running the following command, then refreshing this page:</p>
<code>scrutiny-collector-metrics run</code>
@@ -34,6 +34,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
temperatureOptions: ApexOptions;
tempDurationKey = 'forever'
config: AppConfig;
showArchived: boolean;
// Private
private _unsubscribeAll: Subject<void>;
@@ -257,6 +258,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"