adding typescript interfaces for type hinting and testing

some code reformatting
adding tests for services and components.
cleanup of unused dependencies in components.
refactor dashboard service so that wrapper is removed before data is passed to component. (no more this.data.data...).
refactored components so that variable names are consistent (dashboardService vs smartService).
ensure argument and return types are specified everywhere.
adding tests for pipes.

adding ng test to ci steps.

change dir before running npm install.

trying to install nodejs in continer.

test frontend separately.

upload coverage for frontend and backend.

upload coverage for frontend and backend.

testing coverage file locations.

retry file upload.
pull/328/head
Jason Kulatunga 3 years ago
parent 0e2fec4e93
commit b71d6660a6

@ -3,11 +3,25 @@ name: CI
on: [pull_request]
jobs:
test:
name: Test
test-frontend:
name: Test Frontend
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Test Frontend
run: |
make binary-frontend-test-coverage
- name: Upload coverage
uses: actions/upload-artifact@v3
with:
name: coverage
path: ${{ github.workspace }}/webapp/frontend/coverage/lcov.info
retention-days: 1
test-backend:
name: Test Backend
runs-on: ubuntu-latest
container: ghcr.io/packagrio/packagr:latest-golang
# Service containers to run with `build` (Required for end-to-end testing)
services:
influxdb:
@ -22,7 +36,6 @@ jobs:
ports:
- 8086:8086
env:
PROJECT_PATH: /go/src/github.com/analogj/scrutiny
STATIC: true
steps:
- name: Git
@ -32,16 +45,36 @@ jobs:
git --version
- name: Checkout
uses: actions/checkout@v2
- name: Test
- name: Test Backend
run: |
make binary-clean binary-test-coverage
- name: Generate coverage report
- name: Upload coverage
uses: actions/upload-artifact@v3
with:
name: coverage
path: ${{ github.workspace }}/coverage.txt
retention-days: 1
test-coverage:
name: Test Coverage Upload
needs:
- test-backend
- test-frontend
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Download coverage reports
uses: actions/download-artifact@v3
with:
name: coverage
- name: Upload coverage reports
uses: codecov/codecov-action@v2
with:
files: ${{ github.workspace }}/coverage.txt
files: ${{ github.workspace }}/coverage.txt,${{ github.workspace }}/lcov.info
flags: unittests
fail_ci_if_error: true
verbose: true
build:
name: Build ${{ matrix.cfg.goos }}/${{ matrix.cfg.goarch }}
runs-on: ${{ matrix.cfg.on }}

@ -89,6 +89,10 @@ ifneq ($(OS),Windows_NT)
./$(WEB_BINARY_NAME) || true
endif
########################################################################################################################
# Binary
########################################################################################################################
.PHONY: binary-frontend
# reduce logging, disable angular-cli analytics for ci environment
binary-frontend: export NPM_CONFIG_LOGLEVEL = warn
@ -100,6 +104,12 @@ binary-frontend:
npm ci
npm run build:prod -- --output-path=$(CURDIR)/dist
.PHONY: binary-frontend-test-coverage
# reduce logging, disable angular-cli analytics for ci environment
binary-frontend-test-coverage:
cd webapp/frontend
npm ci
npx ng test --watch=false --browsers=ChromeHeadless --code-coverage
########################################################################################################################
# Docker

@ -18,6 +18,7 @@ func GetDevicesSummary(c *gin.Context) {
return
}
//this must match DeviceSummaryWrapper (webapp/backend/pkg/models/device_summary.go)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": map[string]interface{}{

@ -46,3 +46,5 @@ testem.log
Thumbs.db
/dist
/coverage

@ -17,8 +17,8 @@ module.exports = function (config)
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir : require('path').join(__dirname, './coverage/treo'),
reports : ['html', 'lcovonly', 'text-summary'],
dir: require('path').join(__dirname, './coverage'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters : ['progress', 'kjhtml'],

@ -1,22 +1,28 @@
import { Layout } from 'app/layout/layout.types';
import {Layout} from 'app/layout/layout.types';
// Theme type
export type Theme = 'light' | 'dark' | 'system';
// Device title to display on the dashboard
export type DashboardDisplay = 'name' | 'serial_id' | 'uuid' | 'label'
export type DashboardSort = 'status' | 'title' | 'age'
export type TemperatureUnit = 'celsius' | 'fahrenheit'
/**
* AppConfig interface. Update this interface to strictly type your config
* object.
*/
export interface AppConfig
{
export interface AppConfig {
theme: Theme;
layout: Layout;
// Dashboard options
dashboardDisplay: string;
dashboardSort: string;
dashboardDisplay: DashboardDisplay;
dashboardSort: DashboardSort;
temperatureUnit: string;
temperatureUnit: TemperatureUnit;
}
/**

@ -0,0 +1,14 @@
import {DeviceModel} from 'app/core/models/device-model';
import {SmartModel} from 'app/core/models/measurements/smart-model';
import {AttributeMetadataModel} from 'app/core/models/thresholds/attribute-metadata-model';
// maps to webapp/backend/pkg/models/device_summary.go
export interface DeviceDetailsResponseWrapper {
success: boolean;
errors?: any[];
data: {
device: DeviceModel;
smart_results: SmartModel[];
},
metadata: { [key: string]: AttributeMetadataModel } | { [key: number]: AttributeMetadataModel };
}

@ -1,10 +1,10 @@
// maps to webapp/backend/pkg/models/device.go
export interface DeviceModel {
wwn: string;
device_name: string;
device_uuid: string;
device_serial_id: string;
device_label: string;
device_name?: string;
device_uuid?: string;
device_serial_id?: string;
device_label?: string;
manufacturer: string;
model_name: string;

@ -0,0 +1,16 @@
import {DeviceModel} from 'app/core/models/device-model';
import {SmartTemperatureModel} from 'app/core/models/measurements/smart-temperature-model';
// maps to webapp/backend/pkg/models/device_summary.go
export interface DeviceSummaryModel {
device: DeviceModel;
smart?: SmartSummary;
temp_history?: SmartTemperatureModel[];
}
export interface SmartSummary {
collector_date?: string,
temp?: number
power_on_hours?: number
}

@ -0,0 +1,10 @@
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
// maps to webapp/backend/pkg/models/device_summary.go
export interface DeviceSummaryResponseWrapper {
success: boolean;
errors: any[];
data: {
summary: { [key: string]: DeviceSummaryModel }
}
}

@ -0,0 +1,9 @@
import {SmartTemperatureModel} from './measurements/smart-temperature-model';
export interface DeviceSummaryTempResponseWrapper {
success: boolean;
errors: any[];
data: {
temp_history: { [key: string]: SmartTemperatureModel[]; }
}
}

@ -0,0 +1,19 @@
// maps to webapp/backend/pkg/models/measurements/smart_ata_attribute.go
// maps to webapp/backend/pkg/models/measurements/smart_nvme_attribute.go
// maps to webapp/backend/pkg/models/measurements/smart_scsi_attribute.go
export interface SmartAttributeModel {
attribute_id: number | string
value: number
thresh: number
worst?: number
raw_value?: number
raw_string?: string
when_failed?: string
transformed_value: number
status: number
status_reason?: string
failure_rate?: number
chartData?: any[]
}

@ -0,0 +1,13 @@
// maps to webapp/backend/pkg/models/measurements/smart.go
import {SmartAttributeModel} from './smart-attribute-model';
export interface SmartModel {
date: string;
device_wwn: string;
device_protocol: string;
temp: number;
power_on_hours: number;
power_cycle_count: number
attrs: { [key: string]: SmartAttributeModel }
}

@ -0,0 +1,6 @@
// maps to webapp/backend/pkg/models/measurements/smart_temperature.go
export interface SmartTemperatureModel {
date: string;
temp: number;
}

@ -0,0 +1,13 @@
// map to webapp/backend/pkg/thresholds/ata_attribute_metadata.go
// map to webapp/backend/pkg/thresholds/nvme_attribute_metadata.go
// map to webapp/backend/pkg/thresholds/scsi_attribute_metadata.go
export interface AttributeMetadataModel {
display_name: string
ideal: string
critical: boolean
description: string
transform_value_unit?: string
observed_thresholds?: any[]
display_type: string
}

File diff suppressed because it is too large Load Diff

@ -1,25 +1,64 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {DashboardDeviceDeleteDialogComponent} from './dashboard-device-delete-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 {DashboardDeviceDeleteDialogService} from './dashboard-device-delete-dialog.service';
import {of} from 'rxjs';
import { DashboardDeviceDeleteDialogComponent } from './dashboard-device-delete-dialog.component';
describe('DashboardDeviceDeleteDialogComponent', () => {
let component: DashboardDeviceDeleteDialogComponent;
let fixture: ComponentFixture<DashboardDeviceDeleteDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardDeviceDeleteDialogComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardDeviceDeleteDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
let component: DashboardDeviceDeleteDialogComponent;
let fixture: ComponentFixture<DashboardDeviceDeleteDialogComponent>;
const matDialogRefSpy = jasmine.createSpyObj('MatDialogRef', ['closeDialog', 'close']);
const dashboardDeviceDeleteDialogServiceSpy = jasmine.createSpyObj('DashboardDeviceDeleteDialogService', ['deleteDevice']);
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: DashboardDeviceDeleteDialogService, useValue: dashboardDeviceDeleteDialogServiceSpy}
],
declarations: [DashboardDeviceDeleteDialogComponent]
})
.compileComponents()
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardDeviceDeleteDialogComponent);
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 delete device if delete is clicked', () => {
dashboardDeviceDeleteDialogServiceSpy.deleteDevice.and.returnValue(of({'success': true}));
component.onDeleteClick()
expect(dashboardDeviceDeleteDialogServiceSpy.deleteDevice).toHaveBeenCalledWith('test-wwn');
expect(dashboardDeviceDeleteDialogServiceSpy.deleteDevice.calls.count())
.withContext('one call')
.toBe(1);
});
});

@ -1,7 +1,6 @@
import { Component, OnInit, Inject } from '@angular/core';
import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {DashboardDeviceDeleteDialogService} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.service';
import {Subject} from 'rxjs';
@Component({
selector: 'app-dashboard-device-delete-dialog',

@ -1,44 +1,21 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { Overlay } from '@angular/cdk/overlay';
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { SharedModule } from 'app/shared/shared.module';
import {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 {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component'
import { MatButtonToggleModule} from '@angular/material/button-toggle';
import {MatTabsModule} from '@angular/material/tabs';
import {MatSliderModule} from '@angular/material/slider';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {MatTooltipModule} from '@angular/material/tooltip';
import {dashboardRoutes} from 'app/modules/dashboard/dashboard.routing';
import {MatDividerModule} from '@angular/material/divider';
import {MatMenuModule} from '@angular/material/menu';
import {MatProgressBarModule} from '@angular/material/progress-bar';
import {MatSortModule} from '@angular/material/sort';
import {MatTableModule} from '@angular/material/table';
import {NgApexchartsModule} from 'ng-apexcharts';
import { MatDialogModule } from '@angular/material/dialog';
import {MatDialogModule} from '@angular/material/dialog';
@NgModule({
declarations: [
DashboardDeviceDeleteDialogComponent
],
imports : [
imports: [
RouterModule.forChild([]),
RouterModule.forChild(dashboardRoutes),
MatButtonModule,
MatDividerModule,
MatTooltipModule,
MatIconModule,
MatMenuModule,
MatProgressBarModule,
MatSortModule,
MatTableModule,
NgApexchartsModule,
SharedModule,
MatDialogModule
],

@ -1,25 +1,105 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { DashboardDeviceComponent } from './dashboard-device.component';
import {DashboardDeviceComponent} from './dashboard-device.component';
import {MatDialog} from '@angular/material/dialog';
import {MatButtonModule} from '@angular/material/button';
import {MatIconModule} from '@angular/material/icon';
import {SharedModule} from 'app/shared/shared.module';
import {MatMenuModule} from '@angular/material/menu';
import {TREO_APP_CONFIG} from '@treo/services/config/config.constants';
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
import * as moment from 'moment';
describe('DashboardDeviceComponent', () => {
let component: DashboardDeviceComponent;
let fixture: ComponentFixture<DashboardDeviceComponent>;
let component: DashboardDeviceComponent;
let fixture: ComponentFixture<DashboardDeviceComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardDeviceComponent ]
const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']);
// const configServiceSpy = jasmine.createSpyObj('TreoConfigService', ['config$']);
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MatButtonModule,
MatIconModule,
MatMenuModule,
SharedModule,
],
providers: [
{provide: MatDialog, useValue: matDialogSpy},
{provide: TREO_APP_CONFIG, useValue: {dashboardDisplay: 'name'}}
],
declarations: [DashboardDeviceComponent]
})
.compileComponents();
}));
beforeEach(() => {
// configServiceSpy.config$.and.returnValue(of({'success': true}));
fixture = TestBed.createComponent(DashboardDeviceComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('#classDeviceLastUpdatedOn()', () => {
it('if non-zero device status, should be red', () => {
// component.deviceSummary = summary.data.summary['0x5000c500673e6b5f'] as DeviceSummaryModel
expect(component.classDeviceLastUpdatedOn({
device: {
device_status: 2
}
} as DeviceSummaryModel)).toBe('text-red')
});
it('if non-zero device status, should be red', () => {
// component.deviceSummary = summary.data.summary['0x5000c500673e6b5f'] as DeviceSummaryModel
expect(component.classDeviceLastUpdatedOn({
device: {
device_status: 2
}
} as DeviceSummaryModel)).toBe('text-red')
});
it('if healthy device status and updated in the last two weeks, should be green', () => {
// component.deviceSummary = summary.data.summary['0x5000c500673e6b5f'] as DeviceSummaryModel
expect(component.classDeviceLastUpdatedOn({
device: {
device_status: 0
},
smart: {
collector_date: moment().subtract(13, 'days').toISOString()
}
} as DeviceSummaryModel)).toBe('text-green')
});
it('if healthy device status and updated more than two weeks ago, but less than 1 month, should be yellow', () => {
// component.deviceSummary = summary.data.summary['0x5000c500673e6b5f'] as DeviceSummaryModel
expect(component.classDeviceLastUpdatedOn({
device: {
device_status: 0
},
smart: {
collector_date: moment().subtract(3, 'weeks').toISOString()
}
} as DeviceSummaryModel)).toBe('text-yellow')
});
it('if healthy device status and updated more 1 month ago, should be red', () => {
// component.deviceSummary = summary.data.summary['0x5000c500673e6b5f'] as DeviceSummaryModel
expect(component.classDeviceLastUpdatedOn({
device: {
device_status: 0
},
smart: {
collector_date: moment().subtract(5, 'weeks').toISOString()
}
} as DeviceSummaryModel)).toBe('text-red')
});
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardDeviceComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -1,18 +1,19 @@
import { Component, Input, Output, OnInit, EventEmitter} from '@angular/core';
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import * as moment from 'moment';
import {takeUntil} from 'rxjs/operators';
import {AppConfig} from 'app/core/config/app.config';
import {TreoConfigService} from '@treo/services/config';
import {Subject} from 'rxjs';
import humanizeDuration from 'humanize-duration'
import humanizeDuration from 'humanize-duration'
import {MatDialog} from '@angular/material/dialog';
import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component';
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
@Component({
selector: 'app-dashboard-device',
templateUrl: './dashboard-device.component.html',
styleUrls: ['./dashboard-device.component.scss']
selector: 'app-dashboard-device',
templateUrl: './dashboard-device.component.html',
styleUrls: ['./dashboard-device.component.scss']
})
export class DashboardDeviceComponent implements OnInit {
@ -23,7 +24,8 @@ export class DashboardDeviceComponent implements OnInit {
// Set the private defaults
this._unsubscribeAll = new Subject();
}
@Input() deviceSummary: any;
@Input() deviceSummary: DeviceSummaryModel;
@Input() deviceWWN: string;
@Output() deviceDeleted = new EventEmitter<string>();
@ -47,28 +49,27 @@ export class DashboardDeviceComponent implements OnInit {
// @ Public methods
// -----------------------------------------------------------------------------------------------------
classDeviceLastUpdatedOn(deviceSummary): string {
classDeviceLastUpdatedOn(deviceSummary: DeviceSummaryModel): string {
if (deviceSummary.device.device_status !== 0) {
return 'text-red' // if the device has failed, always highlight in red
} else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){
if(moment().subtract(14, 'd').isBefore(deviceSummary.smart.collector_date)){
} else if (deviceSummary.device.device_status === 0 && deviceSummary.smart) {
if (moment().subtract(14, 'days').isBefore(deviceSummary.smart.collector_date)) {
// this device was updated in the last 2 weeks.
return 'text-green'
} else if(moment().subtract(1, 'm').isBefore(deviceSummary.smart.collector_date)){
} else if (moment().subtract(1, 'months').isBefore(deviceSummary.smart.collector_date)) {
// this device was updated in the last month
return 'text-yellow'
} else{
} else {
// last updated more than a month ago.
return 'text-red'
}
} else {
return ''
}
}
deviceStatusString(deviceStatus): string {
if(deviceStatus === 0){
deviceStatusString(deviceStatus: number): string {
if (deviceStatus === 0) {
return 'passed'
} else {
return 'failed'
@ -76,16 +77,18 @@ export class DashboardDeviceComponent implements OnInit {
}
openDeleteDialog(): void {
const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, {
// width: '250px',
data: {wwn: this.deviceWWN, title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboardDisplay)}
data: {
wwn: this.deviceWWN,
title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboardDisplay)
}
});
dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed', result);
if(result.success){
if (result.success) {
this.deviceDeleted.emit(this.deviceWWN)
}
});

@ -1,53 +1,30 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { Overlay } from '@angular/cdk/overlay';
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { SharedModule } from 'app/shared/shared.module';
import {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 {DashboardDeviceComponent} from 'app/layout/common/dashboard-device/dashboard-device.component'
import { MatDialogModule } from '@angular/material/dialog';
import { MatButtonToggleModule} from '@angular/material/button-toggle';
import {MatTabsModule} from '@angular/material/tabs';
import {MatSliderModule} from '@angular/material/slider';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {MatTooltipModule} from '@angular/material/tooltip';
import {dashboardRoutes} from '../../../modules/dashboard/dashboard.routing';
import {MatDividerModule} from '@angular/material/divider';
import {MatMenuModule} from '@angular/material/menu';
import {MatProgressBarModule} from '@angular/material/progress-bar';
import {MatSortModule} from '@angular/material/sort';
import {MatTableModule} from '@angular/material/table';
import {NgApexchartsModule} from 'ng-apexcharts';
import {DashboardDeviceDeleteDialogModule} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module';
@NgModule({
declarations: [
DashboardDeviceComponent
],
imports : [
imports: [
RouterModule.forChild([]),
RouterModule.forChild(dashboardRoutes),
MatButtonModule,
MatDividerModule,
MatTooltipModule,
MatIconModule,
MatMenuModule,
MatProgressBarModule,
MatSortModule,
MatTableModule,
NgApexchartsModule,
SharedModule,
DashboardDeviceDeleteDialogModule
],
exports : [
exports: [
DashboardDeviceComponent,
],
providers : []
providers: []
})
export class DashboardDeviceModule
{
export class DashboardDeviceModule {
}

@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardSettingsComponent } from './dashboard-settings.component';
describe('DashboardSettingsComponent', () => {
let component: DashboardSettingsComponent;
let fixture: ComponentFixture<DashboardSettingsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardSettingsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardSettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -1,13 +1,13 @@
import { Component, OnInit } from '@angular/core';
import {Component, OnInit} from '@angular/core';
import {AppConfig} from 'app/core/config/app.config';
import { TreoConfigService } from '@treo/services/config';
import {TreoConfigService} from '@treo/services/config';
import {Subject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
@Component({
selector: 'app-dashboard-settings',
templateUrl: './dashboard-settings.component.html',
styleUrls: ['./dashboard-settings.component.scss']
selector: 'app-dashboard-settings',
templateUrl: './dashboard-settings.component.html',
styleUrls: ['./dashboard-settings.component.scss']
})
export class DashboardSettingsComponent implements OnInit {
@ -26,25 +26,23 @@ export class DashboardSettingsComponent implements OnInit {
this._unsubscribeAll = new Subject();
}
ngOnInit(): void {
// Subscribe to config changes
this._configService.config$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((config: AppConfig) => {
ngOnInit(): void {
// Subscribe to config changes
this._configService.config$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((config: AppConfig) => {
// Store the config
this.dashboardDisplay = config.dashboardDisplay;
this.dashboardSort = config.dashboardSort;
this.temperatureUnit = config.temperatureUnit;
this.theme = config.theme;
// Store the config
this.dashboardDisplay = config.dashboardDisplay;
this.dashboardSort = config.dashboardSort;
this.temperatureUnit = config.temperatureUnit;
this.theme = config.theme;
});
}
saveSettings(): void {
});
}
saveSettings(): void {
const newSettings = {
dashboardDisplay: this.dashboardDisplay,
dashboardSort: this.dashboardSort,
@ -53,7 +51,7 @@ export class DashboardSettingsComponent implements OnInit {
}
this._configService.config = newSettings
console.log(`Saved Settings: ${JSON.stringify(newSettings)}`)
}
}
formatLabel(value: number): number {
return value;

@ -1,16 +1,15 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { Overlay } from '@angular/cdk/overlay';
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { SharedModule } from 'app/shared/shared.module';
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {MatAutocompleteModule} from '@angular/material/autocomplete';
import {MatButtonModule} from '@angular/material/button';
import {MatSelectModule} from '@angular/material/select';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {SharedModule} from 'app/shared/shared.module';
import {DetailSettingsComponent} from 'app/layout/common/detail-settings/detail-settings.component'
import { MatDialogModule } from '@angular/material/dialog';
import { MatButtonToggleModule} from '@angular/material/button-toggle';
import {MatDialogModule} from '@angular/material/dialog';
import {MatButtonToggleModule} from '@angular/material/button-toggle';
import {MatTabsModule} from '@angular/material/tabs';
import {MatSliderModule} from '@angular/material/slider';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
@ -20,7 +19,7 @@ import {MatTooltipModule} from '@angular/material/tooltip';
declarations: [
DetailSettingsComponent
],
imports : [
imports: [
RouterModule.forChild([]),
MatAutocompleteModule,
MatDialogModule,
@ -36,11 +35,10 @@ import {MatTooltipModule} from '@angular/material/tooltip';
MatSlideToggleModule,
SharedModule
],
exports : [
exports: [
DetailSettingsComponent
],
providers : []
providers: []
})
export class DetailSettingsModule
{
export class DetailSettingsModule {
}

@ -1,5 +1,5 @@
<div *ngIf="data && data.data && data.data.summary; else emptyDashboard">
<div *ngIf="summaryData; else emptyDashboard">
<div class="flex flex-col flex-auto w-full p-8 xs:p-2">
<div class="flex flex-wrap w-full">

@ -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

@ -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<any>
{
export class DashboardResolver implements Resolve<any> {
/**
* Constructor
*
@ -29,8 +29,7 @@ export class DashboardResolver implements Resolve<any>
* @param route
* @param state
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any>
{
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<{ [p: string]: DeviceSummaryModel }> {
return this._dashboardService.getSummaryData();
}
}

@ -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<HttpClient>;
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);
});
});

@ -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<any>;
private _data: BehaviorSubject<{ [p: string]: DeviceSummaryModel }>;
/**
* Constructor
@ -32,8 +35,7 @@ export class DashboardService
/**
* Getter for data
*/
get data$(): Observable<any>
{
get data$(): Observable<{ [p: string]: DeviceSummaryModel }> {
return this._data.asObservable();
}
@ -44,22 +46,28 @@ export class DashboardService
/**
* Get data
*/
getSummaryData(): Observable<any>
{
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<any>
{
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
})
);
}
}

@ -1,38 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientModule } from '@angular/common/http';
import { DetailComponent } from './detail.component';
import {TreoConfigService} from '@treo/services/config';
import { TREO_APP_CONFIG } from '@treo/services/config/config.constants';
const TREO_APP_CONFIG_PROVIDER = [ { provide: TREO_APP_CONFIG, useValue: TreoConfigService } ];
import { MatDialogModule } from '@angular/material/dialog';
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
describe('DetailComponent', () => {
let component: DetailComponent;
let fixture: ComponentFixture<DetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule,
MatDialogModule
],
declarations: [ DetailComponent, DeviceTitlePipe ],
providers: [ TREO_APP_CONFIG_PROVIDER ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<ApexOptions>;
smartAttributeDataSource: MatTableDataSource<any>;
smartAttributeDataSource: MatTableDataSource<SmartAttributeModel>;
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;
}

@ -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<any>
{
export class DetailResolver implements Resolve<any> {
/**
* Constructor
*
@ -29,8 +29,7 @@ export class DetailResolver implements Resolve<any>
* @param route
* @param state
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any>
{
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<DeviceDetailsResponseWrapper> {
return this._detailService.getData(route.params.wwn);
}
}

@ -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<HttpClient>;
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);
});
})
});

@ -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<any>;
private _data: BehaviorSubject<DeviceDetailsResponseWrapper>;
/**
* 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<any>
{
get data$(): Observable<DeviceDetailsResponseWrapper> {
return this._data.asObservable();
}
@ -44,10 +42,9 @@ export class DetailService
/**
* Get data
*/
getData(wwn): Observable<any>
{
getData(wwn): Observable<DeviceDetailsResponseWrapper> {
return this._httpClient.get(getBasePath() + `/api/device/${wwn}/details`).pipe(
tap((response: any) => {
tap((response: DeviceDetailsResponseWrapper) => {
this._data.next(response);
})
);

@ -1,14 +1,13 @@
import { DeviceTitlePipe } from './device-title.pipe';
import {FileSizePipe} from "./file-size.pipe";
import {DeviceModel} from "../core/models/device-model";
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',() => {
describe('#deviceTitleForType', () => {
const testCases = [
{
'device': {

@ -1,4 +1,4 @@
import { Pipe, PipeTransform } from '@angular/core';
import {Pipe, PipeTransform} from '@angular/core';
import {DeviceModel} from 'app/core/models/device-model';
@Pipe({
@ -36,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)
@ -48,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)
}

Loading…
Cancel
Save