diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts
index eb5a2cc..7352e98 100644
--- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts
+++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts
@@ -1,17 +1,24 @@
-import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
-import { MatSort } from '@angular/material/sort';
-import { MatTableDataSource } from '@angular/material/table';
-import { Subject } from 'rxjs';
-import { takeUntil } from 'rxjs/operators';
+import {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ Component,
+ OnDestroy,
+ OnInit,
+ ViewChild,
+ ViewEncapsulation
+} from '@angular/core';
+import {Subject} from 'rxjs';
+import {takeUntil} from 'rxjs/operators';
import {ApexOptions, ChartComponent} from 'ng-apexcharts';
-import { DashboardService } from 'app/modules/dashboard/dashboard.service';
+import {DashboardService} from 'app/modules/dashboard/dashboard.service';
import {MatDialog} from '@angular/material/dialog';
-import { DashboardSettingsComponent } from 'app/layout/common/dashboard-settings/dashboard-settings.component';
+import {DashboardSettingsComponent} from 'app/layout/common/dashboard-settings/dashboard-settings.component';
import {AppConfig} from 'app/core/config/app.config';
import {TreoConfigService} from '@treo/services/config';
import {Router} from '@angular/router';
import {TemperaturePipe} from 'app/shared/temperature.pipe';
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
+import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
@Component({
selector : 'example',
@@ -22,7 +29,7 @@ import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
})
export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
{
- data: any;
+ summaryData: { [key: string]: DeviceSummaryModel };
hostGroups: { [hostId: string]: string[] } = {}
temperatureOptions: ApexOptions;
tempDurationKey = 'forever'
@@ -35,10 +42,13 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
/**
* Constructor
*
- * @param {SmartService} _smartService
+ * @param {DashboardService} _dashboardService
+ * @param {TreoConfigService} _configService
+ * @param {MatDialog} dialog
+ * @param {Router} router
*/
constructor(
- private _smartService: DashboardService,
+ private _dashboardService: DashboardService,
private _configService: TreoConfigService,
public dialog: MatDialog,
private router: Router,
@@ -81,16 +91,16 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
});
// Get the data
- this._smartService.data$
+ this._dashboardService.data$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((data) => {
// Store the data
- this.data = data;
+ this.summaryData = data;
// generate group data.
- for(const wwn in this.data.data.summary){
- const hostid = this.data.data.summary[wwn].device.host_id
+ for (const wwn in this.summaryData) {
+ const hostid = this.summaryData[wwn].device.host_id
const hostDeviceList = this.hostGroups[hostid] || []
hostDeviceList.push(wwn)
this.hostGroups[hostid] = hostDeviceList
@@ -132,11 +142,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
private _deviceDataTemperatureSeries(): any[] {
const deviceTemperatureSeries = []
- console.log('DEVICE DATA SUMMARY', this.data)
+ console.log('DEVICE DATA SUMMARY', this.summaryData)
- for(const wwn in this.data.data.summary){
- const deviceSummary = this.data.data.summary[wwn]
- if (!deviceSummary.temp_history){
+ for (const wwn in this.summaryData) {
+ const deviceSummary = this.summaryData[wwn]
+ if (!deviceSummary.temp_history) {
continue
}
@@ -206,7 +216,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
}
}
},
- xaxis : {
+ xaxis: {
type: 'datetime'
}
};
@@ -216,11 +226,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
// @ Public methods
// -----------------------------------------------------------------------------------------------------
- deviceSummariesForHostGroup(hostGroupWWNs: string[]): any[] {
- const deviceSummaries = []
- for(const wwn of hostGroupWWNs){
- if(this.data.data.summary[wwn]){
- deviceSummaries.push(this.data.data.summary[wwn])
+ deviceSummariesForHostGroup(hostGroupWWNs: string[]): DeviceSummaryModel[] {
+ const deviceSummaries: DeviceSummaryModel[] = []
+ for (const wwn of hostGroupWWNs) {
+ if (this.summaryData[wwn]) {
+ deviceSummaries.push(this.summaryData[wwn])
}
}
return deviceSummaries
@@ -235,7 +245,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
}
onDeviceDeleted(wwn: string): void {
- delete this.data.data.summary[wwn] // remove the device from the summary list.
+ delete this.summaryData[wwn] // remove the device from the summary list.
}
/*
@@ -246,16 +256,16 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
DURATION_KEY_FOREVER = "forever"
*/
- changeSummaryTempDuration(durationKey: string){
+ changeSummaryTempDuration(durationKey: string): void {
this.tempDurationKey = durationKey
- this._smartService.getSummaryTempData(durationKey)
- .subscribe((data) => {
+ this._dashboardService.getSummaryTempData(durationKey)
+ .subscribe((tempHistoryData) => {
// given a list of device temp history, override the data in the "summary" object.
- for(const wwn in this.data.data.summary) {
+ for (const wwn in this.summaryData) {
// 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] || []
+ this.summaryData[wwn].temp_history = tempHistoryData[wwn] || []
}
// Prepare the chart series data
diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts
index eb29b48..a235061 100644
--- a/webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts
+++ b/webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts
@@ -1,13 +1,13 @@
-import { Injectable } from '@angular/core';
-import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
-import { Observable } from 'rxjs';
-import { DashboardService } from 'app/modules/dashboard/dashboard.service';
+import {Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router';
+import {Observable} from 'rxjs';
+import {DashboardService} from 'app/modules/dashboard/dashboard.service';
+import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
@Injectable({
providedIn: 'root'
})
-export class DashboardResolver implements Resolve
-{
+export class DashboardResolver implements Resolve {
/**
* Constructor
*
@@ -29,8 +29,7 @@ export class DashboardResolver implements Resolve
* @param route
* @param state
*/
- resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable
- {
+ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<{ [p: string]: DeviceSummaryModel }> {
return this._dashboardService.getSummaryData();
}
}
diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.service.spec.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.service.spec.ts
new file mode 100644
index 0000000..75a7679
--- /dev/null
+++ b/webapp/frontend/src/app/modules/dashboard/dashboard.service.spec.ts
@@ -0,0 +1,44 @@
+import {HttpClient} from '@angular/common/http';
+import {DashboardService} from './dashboard.service';
+import {of} from 'rxjs';
+import {summary} from 'app/data/mock/summary/data'
+import {temp_history} from 'app/data/mock/summary/temp_history'
+import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
+import {SmartTemperatureModel} from 'app/core/models/measurements/smart-temperature-model';
+
+describe('DashboardService', () => {
+ let service: DashboardService;
+ let httpClientSpy: jasmine.SpyObj;
+
+ beforeEach(() => {
+ httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
+ service = new DashboardService(httpClientSpy);
+ });
+
+ it('should unwrap and return getSummaryData() (HttpClient called once)', (done: DoneFn) => {
+ httpClientSpy.get.and.returnValue(of(summary));
+
+ service.getSummaryData().subscribe(value => {
+ expect(value).toBe(summary.data.summary as { [key: string]: DeviceSummaryModel });
+ done();
+ });
+ expect(httpClientSpy.get.calls.count())
+ .withContext('one call')
+ .toBe(1);
+ });
+
+ it('should unwrap and return getSummaryTempData() (HttpClient called once)', (done: DoneFn) => {
+ // const expectedHeroes: any[] =
+ // [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
+
+ httpClientSpy.get.and.returnValue(of(temp_history));
+
+ service.getSummaryTempData('weekly').subscribe(value => {
+ expect(value).toBe(temp_history.data.temp_history as { [key: string]: SmartTemperatureModel[] });
+ done();
+ });
+ expect(httpClientSpy.get.calls.count())
+ .withContext('one call')
+ .toBe(1);
+ });
+});
diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts
index 185da9d..35764d0 100644
--- a/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts
+++ b/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts
@@ -1,16 +1,19 @@
-import { Injectable } from '@angular/core';
-import { HttpClient } from '@angular/common/http';
-import { BehaviorSubject, Observable } from 'rxjs';
-import { tap } from 'rxjs/operators';
-import { getBasePath } from 'app/app.routing';
+import {Injectable} from '@angular/core';
+import {HttpClient} from '@angular/common/http';
+import {BehaviorSubject, Observable} from 'rxjs';
+import {map, tap} from 'rxjs/operators';
+import {getBasePath} from 'app/app.routing';
+import {DeviceSummaryResponseWrapper} from 'app/core/models/device-summary-response-wrapper';
+import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
+import {SmartTemperatureModel} from 'app/core/models/measurements/smart-temperature-model';
+import {DeviceSummaryTempResponseWrapper} from 'app/core/models/device-summary-temp-response-wrapper';
@Injectable({
providedIn: 'root'
})
-export class DashboardService
-{
+export class DashboardService {
// Observables
- private _data: BehaviorSubject;
+ private _data: BehaviorSubject<{ [p: string]: DeviceSummaryModel }>;
/**
* Constructor
@@ -32,8 +35,7 @@ export class DashboardService
/**
* Getter for data
*/
- get data$(): Observable
- {
+ get data$(): Observable<{ [p: string]: DeviceSummaryModel }> {
return this._data.asObservable();
}
@@ -44,22 +46,28 @@ export class DashboardService
/**
* Get data
*/
- getSummaryData(): Observable
- {
+ getSummaryData(): Observable<{ [key: string]: DeviceSummaryModel }> {
return this._httpClient.get(getBasePath() + '/api/summary').pipe(
- tap((response: any) => {
+ map((response: DeviceSummaryResponseWrapper) => {
+ // console.log("FILTERING=----", response.data.summary)
+ return response.data.summary
+ }),
+ tap((response: { [key: string]: DeviceSummaryModel }) => {
this._data.next(response);
})
);
}
- getSummaryTempData(durationKey: string): Observable
- {
+ getSummaryTempData(durationKey: string): Observable<{ [key: string]: SmartTemperatureModel[] }> {
const params = {}
- if(durationKey){
+ if (durationKey) {
params['duration_key'] = durationKey
}
- return this._httpClient.get(getBasePath() + '/api/summary/temp', {params: params});
+ return this._httpClient.get(getBasePath() + '/api/summary/temp', {params: params}).pipe(
+ map((response: DeviceSummaryTempResponseWrapper) => {
+ return response.data.temp_history
+ })
+ );
}
}
diff --git a/webapp/frontend/src/app/modules/detail/detail.component.spec.ts b/webapp/frontend/src/app/modules/detail/detail.component.spec.ts
deleted file mode 100644
index 149b9be..0000000
--- a/webapp/frontend/src/app/modules/detail/detail.component.spec.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { DetailComponent } from './detail.component';
-
-describe('DetailComponent', () => {
- let component: DetailComponent;
- let fixture: ComponentFixture;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [ DetailComponent ]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(DetailComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/webapp/frontend/src/app/modules/detail/detail.component.ts b/webapp/frontend/src/app/modules/detail/detail.component.ts
index f37910a..0e29652 100644
--- a/webapp/frontend/src/app/modules/detail/detail.component.ts
+++ b/webapp/frontend/src/app/modules/detail/detail.component.ts
@@ -1,18 +1,21 @@
-import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import humanizeDuration from 'humanize-duration';
+import {AfterViewInit, Component, Inject, LOCALE_ID, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {ApexOptions} from 'ng-apexcharts';
-import {MatTableDataSource} from '@angular/material/table';
-import {MatSort} from '@angular/material/sort';
-import {Subject} from 'rxjs';
+import {AppConfig} from 'app/core/config/app.config';
import {DetailService} from './detail.service';
-import {takeUntil} from 'rxjs/operators';
import {DetailSettingsComponent} from 'app/layout/common/detail-settings/detail-settings.component';
import {MatDialog} from '@angular/material/dialog';
-import humanizeDuration from 'humanize-duration';
+import {MatSort} from '@angular/material/sort';
+import {MatTableDataSource} from '@angular/material/table';
+import {Subject} from 'rxjs';
import {TreoConfigService} from '@treo/services/config';
-import {AppConfig} from 'app/core/config/app.config';
import {animate, state, style, transition, trigger} from '@angular/animations';
import {formatDate} from '@angular/common';
-import { LOCALE_ID, Inject } from '@angular/core';
+import {takeUntil} from 'rxjs/operators';
+import {DeviceModel} from 'app/core/models/device-model';
+import {SmartModel} from 'app/core/models/measurements/smart-model';
+import {SmartAttributeModel} from 'app/core/models/measurements/smart-attribute-model';
+import {AttributeMetadataModel} from 'app/core/models/thresholds/attribute-metadata-model';
// from Constants.go - these must match
const AttributeStatusPassed = 0
@@ -22,9 +25,9 @@ const AttributeStatusFailedScrutiny = 4
@Component({
- selector: 'detail',
- templateUrl: './detail.component.html',
- styleUrls: ['./detail.component.scss'],
+ selector: 'detail',
+ templateUrl: './detail.component.html',
+ styleUrls: ['./detail.component.scss'],
animations: [
trigger('detailExpand', [
state('collapsed', style({height: '0px', minHeight: '0'})),
@@ -40,22 +43,23 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
* Constructor
*
* @param {DetailService} _detailService
+ * @param {MatDialog} dialog
+ * @param {TreoConfigService} _configService
+ * @param {string} locale
*/
constructor(
private _detailService: DetailService,
public dialog: MatDialog,
private _configService: TreoConfigService,
@Inject(LOCALE_ID) public locale: string
-
- )
- {
+ ) {
// Set the private defaults
this._unsubscribeAll = new Subject();
// Set the defaults
this.smartAttributeDataSource = new MatTableDataSource();
// this.recentTransactionsTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh'];
- this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh','ideal', 'failure', 'history'];
+ this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh', 'ideal', 'failure', 'history'];
this.systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
@@ -65,14 +69,15 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
onlyCritical = true;
// data: any;
- expandedAttribute: any | null;
+ expandedAttribute: SmartAttributeModel | null;
- metadata: any;
- device: any;
- smart_results: any[];
+ metadata: { [p: string]: AttributeMetadataModel } | { [p: number]: AttributeMetadataModel };
+ device: DeviceModel;
+ // tslint:disable-next-line:variable-name
+ smart_results: SmartModel[];
commonSparklineOptions: Partial;
- smartAttributeDataSource: MatTableDataSource;
+ smartAttributeDataSource: MatTableDataSource;
smartAttributeTableColumns: string[];
@ViewChild('smartAttributeTable', {read: MatSort})
@@ -91,8 +96,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
/**
* On init
*/
- ngOnInit(): void
- {
+ ngOnInit(): void {
// Subscribe to config changes
this._configService.config$
.pipe(takeUntil(this._unsubscribeAll))
@@ -104,13 +108,13 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
// Get the data
this._detailService.data$
.pipe(takeUntil(this._unsubscribeAll))
- .subscribe((data) => {
+ .subscribe((respWrapper) => {
// Store the data
// this.data = data;
- this.device = data.data.device;
- this.smart_results = data.data.smart_results
- this.metadata = data.metadata;
+ this.device = respWrapper.data.device;
+ this.smart_results = respWrapper.data.smart_results
+ this.metadata = respWrapper.metadata;
// Store the table data
@@ -124,8 +128,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
/**
* After view init
*/
- ngAfterViewInit(): void
- {
+ ngAfterViewInit(): void {
// Make the data source sortable
this.smartAttributeDataSource.sort = this.smartAttributeTableMatSort;
}
@@ -133,8 +136,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
/**
* On destroy
*/
- ngOnDestroy(): void
- {
+ ngOnDestroy(): void {
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
@@ -147,22 +149,23 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
getAttributeStatusName(attributeStatus: number): string {
// tslint:disable:no-bitwise
- if(attributeStatus === AttributeStatusPassed){
+ if (attributeStatus === AttributeStatusPassed) {
return 'passed'
- } else if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0 || (attributeStatus & AttributeStatusFailedSmart) !== 0 ){
+ } else if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0 || (attributeStatus & AttributeStatusFailedSmart) !== 0) {
return 'failed'
- } else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0){
+ } else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0) {
return 'warn'
}
return ''
// tslint:enable:no-bitwise
}
+
getAttributeScrutinyStatusName(attributeStatus: number): string {
// tslint:disable:no-bitwise
- if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0){
+ if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0) {
return 'failed'
- } else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0){
+ } else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0) {
return 'warn'
} else {
return 'passed'
@@ -172,7 +175,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
getAttributeSmartStatusName(attributeStatus: number): string {
// tslint:disable:no-bitwise
- if ((attributeStatus & AttributeStatusFailedSmart) !== 0){
+ if ((attributeStatus & AttributeStatusFailedSmart) !== 0) {
return 'failed'
} else {
return 'passed'
@@ -181,138 +184,140 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
}
- getAttributeName(attribute_data): string {
- const attribute_metadata = this.metadata[attribute_data.attribute_id]
- if(!attribute_metadata){
+ getAttributeName(attributeData: SmartAttributeModel): string {
+ const attributeMetadata = this.metadata[attributeData.attribute_id]
+ if (!attributeMetadata) {
return 'Unknown Attribute Name'
} else {
- return attribute_metadata.display_name
+ return attributeMetadata.display_name
}
}
- getAttributeDescription(attribute_data){
- const attribute_metadata = this.metadata[attribute_data.attribute_id]
- if(!attribute_metadata){
+
+ getAttributeDescription(attributeData: SmartAttributeModel): string {
+ const attributeMetadata = this.metadata[attributeData.attribute_id]
+ if (!attributeMetadata) {
return 'Unknown'
} else {
- return attribute_metadata.description
+ return attributeMetadata.description
}
- return
}
- getAttributeValue(attribute_data){
- if(this.isAta()) {
- const attribute_metadata = this.metadata[attribute_data.attribute_id]
- if(!attribute_metadata){
- return attribute_data.value
- } else if (attribute_metadata.display_type == 'raw') {
- return attribute_data.raw_value
- } else if (attribute_metadata.display_type == 'transformed' && attribute_data.transformed_value) {
- return attribute_data.transformed_value
+ getAttributeValue(attributeData: SmartAttributeModel): number {
+ if (this.isAta()) {
+ const attributeMetadata = this.metadata[attributeData.attribute_id]
+ if (!attributeMetadata) {
+ return attributeData.value
+ } else if (attributeMetadata.display_type === 'raw') {
+ return attributeData.raw_value
+ } else if (attributeMetadata.display_type === 'transformed' && attributeData.transformed_value) {
+ return attributeData.transformed_value
} else {
- return attribute_data.value
+ return attributeData.value
}
- }
- else{
- return attribute_data.value
+ } else {
+ return attributeData.value
}
}
- getAttributeValueType(attribute_data){
- if(this.isAta()) {
- const attribute_metadata = this.metadata[attribute_data.attribute_id]
- if(!attribute_metadata){
+ getAttributeValueType(attributeData: SmartAttributeModel): string {
+ if (this.isAta()) {
+ const attributeMetadata = this.metadata[attributeData.attribute_id]
+ if (!attributeMetadata) {
return ''
} else {
- return attribute_metadata.display_type
+ return attributeMetadata.display_type
}
} else {
return ''
}
}
- getAttributeIdeal(attribute_data){
- if(this.isAta()){
- return this.metadata[attribute_data.attribute_id]?.display_type == 'raw' ? this.metadata[attribute_data.attribute_id]?.ideal : ''
+ getAttributeIdeal(attributeData: SmartAttributeModel): string {
+ if (this.isAta()) {
+ return this.metadata[attributeData.attribute_id]?.display_type === 'raw' ? this.metadata[attributeData.attribute_id]?.ideal : ''
} else {
- return this.metadata[attribute_data.attribute_id]?.ideal
+ return this.metadata[attributeData.attribute_id]?.ideal
}
}
- getAttributeWorst(attribute_data){
- const attribute_metadata = this.metadata[attribute_data.attribute_id]
- if(!attribute_metadata){
- return attribute_data.worst
+ getAttributeWorst(attributeData: SmartAttributeModel): number | string {
+ const attributeMetadata = this.metadata[attributeData.attribute_id]
+ if (!attributeMetadata) {
+ return attributeData.worst
} else {
- return attribute_metadata?.display_type == 'normalized' ? attribute_data.worst : ''
+ return attributeMetadata?.display_type === 'normalized' ? attributeData.worst : ''
}
}
- getAttributeThreshold(attribute_data){
- if(this.isAta()){
- const attribute_metadata = this.metadata[attribute_data.attribute_id]
- if(!attribute_metadata || attribute_metadata.display_type == 'normalized'){
- return attribute_data.thresh
+ getAttributeThreshold(attributeData: SmartAttributeModel): number | string {
+ if (this.isAta()) {
+ const attributeMetadata = this.metadata[attributeData.attribute_id]
+ if (!attributeMetadata || attributeMetadata.display_type === 'normalized') {
+ return attributeData.thresh
} else {
// if(this.data.metadata[attribute_data.attribute_id].observed_thresholds){
//
// } else {
// }
// return ''
- return attribute_data.thresh
+ return attributeData.thresh
}
} else {
- return (attribute_data.thresh == -1 ? '' : attribute_data.thresh )
+ return (attributeData.thresh === -1 ? '' : attributeData.thresh)
}
}
- getAttributeCritical(attribute_data){
- return this.metadata[attribute_data.attribute_id]?.critical
+ getAttributeCritical(attributeData: SmartAttributeModel): boolean {
+ return this.metadata[attributeData.attribute_id]?.critical
}
- getHiddenAttributes(){
- if (!this.smart_results || this.smart_results.length == 0) {
+
+ getHiddenAttributes(): number {
+ if (!this.smart_results || this.smart_results.length === 0) {
return 0
}
- let attributes_length = 0
+ let attributesLength = 0
const attributes = this.smart_results[0]?.attrs
if (attributes) {
- attributes_length = Object.keys(attributes).length
+ attributesLength = Object.keys(attributes).length
}
- return attributes_length - this.smartAttributeDataSource.data.length
+ return attributesLength - this.smartAttributeDataSource.data.length
}
isAta(): boolean {
- return this.device.device_protocol == 'ATA'
+ return this.device.device_protocol === 'ATA'
}
+
isScsi(): boolean {
- return this.device.device_protocol == 'SCSI'
+ return this.device.device_protocol === 'SCSI'
}
+
isNvme(): boolean {
- return this.device.device_protocol == 'NVMe'
+ return this.device.device_protocol === 'NVMe'
}
- private _generateSmartAttributeTableDataSource(smart_results){
- const smartAttributeDataSource = [];
+ private _generateSmartAttributeTableDataSource(smartResults: SmartModel[]): SmartAttributeModel[] {
+ const smartAttributeDataSource: SmartAttributeModel[] = [];
- if(smart_results.length == 0){
+ if (smartResults.length === 0) {
return smartAttributeDataSource
}
- const latest_smart_result = smart_results[0];
- let attributes = {}
- if(this.isScsi()) {
+ const latestSmartResult = smartResults[0];
+ let attributes: { [p: string]: SmartAttributeModel } = {}
+ if (this.isScsi()) {
this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'history'];
- attributes = latest_smart_result.attrs
- } else if(this.isNvme()){
+ attributes = latestSmartResult.attrs
+ } else if (this.isNvme()) {
this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'ideal', 'history'];
- attributes = latest_smart_result.attrs
+ attributes = latestSmartResult.attrs
} else {
// ATA
- attributes = latest_smart_result.attrs
- this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'thresh','ideal', 'failure', 'history'];
+ attributes = latestSmartResult.attrs
+ this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'thresh', 'ideal', 'failure', 'history'];
}
- for(const attrId in attributes){
+ for (const attrId in attributes) {
const attr = attributes[attrId]
// chart history data
@@ -320,18 +325,18 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
const attrHistory = []
- for (const smart_result of smart_results){
+ for (const smartResult of smartResults) {
// attrHistory.push(this.getAttributeValue(smart_result.attrs[attrId]))
const chartDatapoint = {
- x: formatDate(smart_result.date, 'MMMM dd, yyyy - HH:mm', this.locale),
- y: this.getAttributeValue(smart_result.attrs[attrId])
+ x: formatDate(smartResult.date, 'MMMM dd, yyyy - HH:mm', this.locale),
+ y: this.getAttributeValue(smartResult.attrs[attrId])
}
- const attributeStatusName = this.getAttributeStatusName(smart_result.attrs[attrId].status)
- if(attributeStatusName === 'failed') {
+ const attributeStatusName = this.getAttributeStatusName(smartResult.attrs[attrId].status)
+ if (attributeStatusName === 'failed') {
chartDatapoint['strokeColor'] = '#F05252'
chartDatapoint['fillColor'] = '#F05252'
- } else if (attributeStatusName === 'warn'){
+ } else if (attributeStatusName === 'warn') {
chartDatapoint['strokeColor'] = '#C27803'
chartDatapoint['fillColor'] = '#C27803'
}
@@ -350,7 +355,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
}
// determine when to include the attributes in table.
- if(!this.onlyCritical || this.onlyCritical && this.metadata[attr.attribute_id]?.critical || attr.value < attr.thresh){
+ if (!this.onlyCritical || this.onlyCritical && this.metadata[attr.attribute_id]?.critical || attr.value < attr.thresh) {
smartAttributeDataSource.push(attr)
}
}
@@ -362,8 +367,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
*
* @private
*/
- private _prepareChartData(): void
- {
+ private _prepareChartData(): void {
// Account balance
this.commonSparklineOptions = {
@@ -392,7 +396,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
},
y: {
title: {
- formatter: function(seriesName) {
+ formatter: (seriesName) => {
return '';
}
}
@@ -410,27 +414,28 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
};
}
- private determineTheme(config:AppConfig): string {
+ private determineTheme(config: AppConfig): string {
if (config.theme === 'system') {
return this.systemPrefersDark ? 'dark' : 'light'
} else {
return config.theme
}
}
+
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
- toHex(decimalNumb){
+ toHex(decimalNumb: number | string): string {
return '0x' + Number(decimalNumb).toString(16).padStart(2, '0').toUpperCase()
}
- toggleOnlyCritical(){
+
+ toggleOnlyCritical(): void {
this.onlyCritical = !this.onlyCritical
this.smartAttributeDataSource.data = this._generateSmartAttributeTableDataSource(this.smart_results);
-
}
- openDialog() {
+ openDialog(): void {
const dialogRef = this.dialog.open(DetailSettingsComponent);
dialogRef.afterClosed().subscribe(result => {
@@ -444,8 +449,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
* @param index
* @param item
*/
- trackByFn(index: number, item: any): any
- {
+ trackByFn(index: number, item: any): any {
return index;
// return item.id || index;
}
diff --git a/webapp/frontend/src/app/modules/detail/detail.resolvers.ts b/webapp/frontend/src/app/modules/detail/detail.resolvers.ts
index b416a3d..221cad1 100644
--- a/webapp/frontend/src/app/modules/detail/detail.resolvers.ts
+++ b/webapp/frontend/src/app/modules/detail/detail.resolvers.ts
@@ -1,13 +1,13 @@
-import { Injectable } from '@angular/core';
-import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
-import { Observable } from 'rxjs';
-import { DetailService } from 'app/modules/detail/detail.service';
+import {Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router';
+import {Observable} from 'rxjs';
+import {DetailService} from 'app/modules/detail/detail.service';
+import {DeviceDetailsResponseWrapper} from 'app/core/models/device-details-response-wrapper';
@Injectable({
providedIn: 'root'
})
-export class DetailResolver implements Resolve
-{
+export class DetailResolver implements Resolve {
/**
* Constructor
*
@@ -29,8 +29,7 @@ export class DetailResolver implements Resolve
* @param route
* @param state
*/
- resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable
- {
+ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
return this._detailService.getData(route.params.wwn);
}
}
diff --git a/webapp/frontend/src/app/modules/detail/detail.service.spec.ts b/webapp/frontend/src/app/modules/detail/detail.service.spec.ts
new file mode 100644
index 0000000..ec6a4b9
--- /dev/null
+++ b/webapp/frontend/src/app/modules/detail/detail.service.spec.ts
@@ -0,0 +1,28 @@
+import {HttpClient} from '@angular/common/http';
+import {DetailService} from './detail.service';
+import {of} from 'rxjs';
+import {sda} from 'app/data/mock/device/details/sda'
+import {DeviceDetailsResponseWrapper} from 'app/core/models/device-details-response-wrapper';
+
+describe('DetailService', () => {
+ describe('#getData', () => {
+ let service: DetailService;
+ let httpClientSpy: jasmine.SpyObj;
+
+ beforeEach(() => {
+ httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
+ service = new DetailService(httpClientSpy);
+ });
+ it('should return getData() (HttpClient called once)', (done: DoneFn) => {
+ httpClientSpy.get.and.returnValue(of(sda));
+
+ service.getData('test').subscribe(value => {
+ expect(value).toBe(sda as DeviceDetailsResponseWrapper);
+ done();
+ });
+ expect(httpClientSpy.get.calls.count())
+ .withContext('one call')
+ .toBe(1);
+ });
+ })
+});
diff --git a/webapp/frontend/src/app/modules/detail/detail.service.ts b/webapp/frontend/src/app/modules/detail/detail.service.ts
index 5747571..e75cb8b 100644
--- a/webapp/frontend/src/app/modules/detail/detail.service.ts
+++ b/webapp/frontend/src/app/modules/detail/detail.service.ts
@@ -1,16 +1,16 @@
-import { Injectable } from '@angular/core';
-import { HttpClient } from '@angular/common/http';
-import { BehaviorSubject, Observable } from 'rxjs';
-import { tap } from 'rxjs/operators';
-import { getBasePath } from 'app/app.routing';
+import {Injectable} from '@angular/core';
+import {HttpClient} from '@angular/common/http';
+import {BehaviorSubject, Observable} from 'rxjs';
+import {tap} from 'rxjs/operators';
+import {getBasePath} from 'app/app.routing';
+import {DeviceDetailsResponseWrapper} from 'app/core/models/device-details-response-wrapper';
@Injectable({
providedIn: 'root'
})
-export class DetailService
-{
+export class DetailService {
// Observables
- private _data: BehaviorSubject;
+ private _data: BehaviorSubject;
/**
* Constructor
@@ -19,8 +19,7 @@ export class DetailService
*/
constructor(
private _httpClient: HttpClient
- )
- {
+ ) {
// Set the private defaults
this._data = new BehaviorSubject(null);
}
@@ -32,8 +31,7 @@ export class DetailService
/**
* Getter for data
*/
- get data$(): Observable
- {
+ get data$(): Observable {
return this._data.asObservable();
}
@@ -44,10 +42,9 @@ export class DetailService
/**
* Get data
*/
- getData(wwn): Observable
- {
+ getData(wwn): Observable {
return this._httpClient.get(getBasePath() + `/api/device/${wwn}/details`).pipe(
- tap((response: any) => {
+ tap((response: DeviceDetailsResponseWrapper) => {
this._data.next(response);
})
);
diff --git a/webapp/frontend/src/app/shared/device-title.pipe.spec.ts b/webapp/frontend/src/app/shared/device-title.pipe.spec.ts
index 1d64103..992cf9a 100644
--- a/webapp/frontend/src/app/shared/device-title.pipe.spec.ts
+++ b/webapp/frontend/src/app/shared/device-title.pipe.spec.ts
@@ -1,8 +1,151 @@
-import { DeviceTitlePipe } from './device-title.pipe';
+import {DeviceTitlePipe} from './device-title.pipe';
+import {DeviceModel} from 'app/core/models/device-model';
describe('DeviceTitlePipe', () => {
- it('create an instance', () => {
- const pipe = new DeviceTitlePipe();
- expect(pipe).toBeTruthy();
- });
+ it('create an instance', () => {
+ const pipe = new DeviceTitlePipe();
+ expect(pipe).toBeTruthy();
+ });
+
+ describe('#deviceTitleForType', () => {
+ const testCases = [
+ {
+ 'device': {
+ 'device_name': 'sda',
+ 'device_type': 'ata',
+ 'model_name': 'Samsung',
+ },
+ 'titleType': 'name',
+ 'result': '/dev/sda - Samsung'
+ },{
+ 'device': {
+ 'device_name': 'nvme0',
+ 'device_type': 'nvme',
+ 'model_name': 'Samsung',
+ },
+ 'titleType': 'name',
+ 'result': '/dev/nvme0 - nvme - Samsung'
+ },{
+ 'device': {},
+ 'titleType': 'serial_id',
+ 'result': ''
+ },{
+ 'device': {
+ 'device_serial_id': 'ata-WDC_WD140EDFZ-11AXXXXX_9RXXXXXX',
+ },
+ 'titleType': 'serial_id',
+ 'result': '/by-id/ata-WDC_WD140EDFZ-11AXXXXX_9RXXXXXX'
+ },{
+ 'device': {},
+ 'titleType': 'uuid',
+ 'result': ''
+ },{
+ 'device': {
+ 'device_uuid': 'abcdef-1234-4567-8901'
+ },
+ 'titleType': 'uuid',
+ 'result': '/by-uuid/abcdef-1234-4567-8901'
+ },{
+ 'device': {},
+ 'titleType': 'label',
+ 'result': ''
+ },{
+ 'device': {
+ 'label': 'custom-device-label'
+ },
+ 'titleType': 'label',
+ 'result': 'custom-device-label'
+ },{
+ 'device': {
+ 'device_label': 'drive-volume-label'
+ },
+ 'titleType': 'label',
+ 'result': '/by-label/drive-volume-label'
+ },
+ ]
+ testCases.forEach((test, index) => {
+ it(`should correctly format device title ${JSON.stringify(test.device)}. (testcase: ${index + 1})`, () => {
+ // test
+ const formatted = DeviceTitlePipe.deviceTitleForType(test.device as DeviceModel, test.titleType)
+ expect(formatted).toEqual(test.result);
+ });
+ })
+ })
+
+ describe('#deviceTitleWithFallback',() => {
+ const testCases = [
+ {
+ 'device': {
+ 'device_name': 'sda',
+ 'device_type': 'ata',
+ 'model_name': 'Samsung',
+ },
+ 'titleType': 'name',
+ 'result': '/dev/sda - Samsung'
+ },{
+ 'device': {
+ 'device_name': 'nvme0',
+ 'device_type': 'nvme',
+ 'model_name': 'Samsung',
+ },
+ 'titleType': 'name',
+ 'result': '/dev/nvme0 - nvme - Samsung'
+ },{
+ 'device': {
+ 'device_name': 'fallback',
+ 'device_type': 'ata',
+ 'model_name': 'fallback',
+ },
+ 'titleType': 'serial_id',
+ 'result': '/dev/fallback - fallback'
+ },{
+ 'device': {
+ 'device_serial_id': 'ata-WDC_WD140EDFZ-11AXXXXX_9RXXXXXX',
+ },
+ 'titleType': 'serial_id',
+ 'result': '/by-id/ata-WDC_WD140EDFZ-11AXXXXX_9RXXXXXX'
+ },{
+ 'device': {
+ 'device_name': 'fallback',
+ 'device_type': 'ata',
+ 'model_name': 'fallback',
+ },
+ 'titleType': 'uuid',
+ 'result': '/dev/fallback - fallback'
+ },{
+ 'device': {
+ 'device_uuid': 'abcdef-1234-4567-8901'
+ },
+ 'titleType': 'uuid',
+ 'result': '/by-uuid/abcdef-1234-4567-8901'
+ },{
+ 'device': {
+ 'device_name': 'fallback',
+ 'device_type': 'ata',
+ 'model_name': 'fallback',
+ },
+ 'titleType': 'label',
+ 'result': '/dev/fallback - fallback'
+ },{
+ 'device': {
+ 'label': 'custom-device-label'
+ },
+ 'titleType': 'label',
+ 'result': 'custom-device-label'
+ },{
+ 'device': {
+ 'device_label': 'drive-volume-label'
+ },
+ 'titleType': 'label',
+ 'result': '/by-label/drive-volume-label'
+ },
+ ]
+ testCases.forEach((test, index) => {
+ it(`should correctly format device title ${JSON.stringify(test.device)}. (testcase: ${index + 1})`, () => {
+ // test
+ const formatted = DeviceTitlePipe.deviceTitleWithFallback(test.device as DeviceModel, test.titleType)
+ expect(formatted).toEqual(test.result);
+ });
+ })
+ })
});
diff --git a/webapp/frontend/src/app/shared/device-title.pipe.ts b/webapp/frontend/src/app/shared/device-title.pipe.ts
index 1196fb8..3cabc0f 100644
--- a/webapp/frontend/src/app/shared/device-title.pipe.ts
+++ b/webapp/frontend/src/app/shared/device-title.pipe.ts
@@ -1,11 +1,12 @@
-import { Pipe, PipeTransform } from '@angular/core';
+import {Pipe, PipeTransform} from '@angular/core';
+import {DeviceModel} from 'app/core/models/device-model';
@Pipe({
name: 'deviceTitle'
})
export class DeviceTitlePipe implements PipeTransform {
- static deviceTitleForType(device: any, titleType: string): string {
+ static deviceTitleForType(device: DeviceModel, titleType: string): string {
const titleParts = []
switch(titleType){
case 'name':
@@ -35,7 +36,7 @@ export class DeviceTitlePipe implements PipeTransform {
return titleParts.join(' - ')
}
- static deviceTitleWithFallback(device, titleType: string): string {
+ static deviceTitleWithFallback(device: DeviceModel, titleType: string): string {
console.log(`Displaying Device ${device.wwn} with: ${titleType}`)
const titleParts = []
if (device.host_id) titleParts.push(device.host_id)
@@ -47,7 +48,7 @@ export class DeviceTitlePipe implements PipeTransform {
}
- transform(device: any, titleType: string = 'name'): string {
+ transform(device: DeviceModel, titleType: string = 'name'): string {
return DeviceTitlePipe.deviceTitleWithFallback(device, titleType)
}
diff --git a/webapp/frontend/src/app/shared/file-size.pipe.spec.ts b/webapp/frontend/src/app/shared/file-size.pipe.spec.ts
new file mode 100644
index 0000000..14973cf
--- /dev/null
+++ b/webapp/frontend/src/app/shared/file-size.pipe.spec.ts
@@ -0,0 +1,35 @@
+import { FileSizePipe } from './file-size.pipe';
+
+describe('FileSizePipe', () => {
+ it('create an instance', () => {
+ const pipe = new FileSizePipe();
+ expect(pipe).toBeTruthy();
+ });
+
+ describe('#transform',() => {
+ const testCases = [
+ {
+ 'bytes': 1500,
+ 'precision': undefined,
+ 'result': '1 KB'
+ },{
+ 'bytes': 2_100_000_000,
+ 'precision': undefined,
+ 'result': '2.0 GB',
+ },{
+ 'bytes': 1500,
+ 'precision': 2,
+ 'result': '1.46 KB',
+ }
+ ]
+ testCases.forEach((test, index) => {
+ it(`should correctly format bytes ${test.bytes}. (testcase: ${index + 1})`, () => {
+ // test
+ const pipe = new FileSizePipe();
+ const formatted = pipe.transform(test.bytes, test.precision)
+ expect(formatted).toEqual(test.result);
+ });
+ })
+ })
+
+});
diff --git a/webapp/frontend/src/app/shared/temperature.pipe.spec.ts b/webapp/frontend/src/app/shared/temperature.pipe.spec.ts
index fc30978..70a4908 100644
--- a/webapp/frontend/src/app/shared/temperature.pipe.spec.ts
+++ b/webapp/frontend/src/app/shared/temperature.pipe.spec.ts
@@ -1,8 +1,83 @@
import { TemperaturePipe } from './temperature.pipe';
describe('TemperaturePipe', () => {
- it('create an instance', () => {
- const pipe = new TemperaturePipe();
- expect(pipe).toBeTruthy();
- });
+ it('create an instance', () => {
+ const pipe = new TemperaturePipe();
+ expect(pipe).toBeTruthy();
+ });
+
+
+ describe('#celsiusToFahrenheit', () => {
+ const testCases = [
+ {
+ 'c': -273.15,
+ 'f': -460,
+ },{
+ 'c': -34.44,
+ 'f': -30,
+ },{
+ 'c': -23.33,
+ 'f': -10,
+ },{
+ 'c': -17.78,
+ 'f': -0,
+ },{
+ 'c': 0,
+ 'f': 32,
+ },{
+ 'c': 10,
+ 'f': 50,
+ },{
+ 'c': 26.67,
+ 'f': 80,
+ },{
+ 'c': 37,
+ 'f': 99,
+ },{
+ 'c': 60,
+ 'f': 140,
+ }
+ ]
+ testCases.forEach((test, index) => {
+ it(`should correctly convert ${test.c}, Celsius to Fahrenheit (testcase: ${index + 1})`, () => {
+ // test
+ const numb = TemperaturePipe.celsiusToFahrenheit(test.c)
+ const roundNumb = Math.round(numb);
+ expect(roundNumb).toEqual(test.f);
+ });
+ })
+ });
+
+ describe('#formatTemperature',() => {
+ const testCases = [
+ {
+ 'c': 26.67,
+ 'unit': 'celsius',
+ 'includeUnits': true,
+ 'result': '26.67°C'
+ },{
+ 'c': 26.67,
+ 'unit': 'celsius',
+ 'includeUnits': false,
+ 'result': '26.67',
+ },{
+ 'c': 26.67,
+ 'unit': 'fahrenheit',
+ 'includeUnits': true,
+ 'result': '80.006°F',
+ },{
+ 'c': 26.67,
+ 'unit': 'fahrenheit',
+ 'includeUnits': false,
+ 'result': '80.006',
+ }
+ ]
+ testCases.forEach((test, index) => {
+ it(`should correctly format temperature ${test.c} to ${test.unit} ${test.includeUnits ? 'with' : 'without'} unit. (testcase: ${index + 1})`, () => {
+ // test
+ const formatted = TemperaturePipe.formatTemperature(test.c, test.unit, test.includeUnits)
+ expect(formatted).toEqual(test.result);
+ });
+ })
+ })
});