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.
This commit is contained in:
@@ -3,11 +3,25 @@ name: CI
|
|||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test-frontend:
|
||||||
name: Test
|
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
|
runs-on: ubuntu-latest
|
||||||
container: ghcr.io/packagrio/packagr:latest-golang
|
container: ghcr.io/packagrio/packagr:latest-golang
|
||||||
|
|
||||||
# Service containers to run with `build` (Required for end-to-end testing)
|
# Service containers to run with `build` (Required for end-to-end testing)
|
||||||
services:
|
services:
|
||||||
influxdb:
|
influxdb:
|
||||||
@@ -22,7 +36,6 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- 8086:8086
|
- 8086:8086
|
||||||
env:
|
env:
|
||||||
PROJECT_PATH: /go/src/github.com/analogj/scrutiny
|
|
||||||
STATIC: true
|
STATIC: true
|
||||||
steps:
|
steps:
|
||||||
- name: Git
|
- name: Git
|
||||||
@@ -32,16 +45,36 @@ jobs:
|
|||||||
git --version
|
git --version
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Test
|
- name: Test Backend
|
||||||
run: |
|
run: |
|
||||||
make binary-clean binary-test-coverage
|
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
|
uses: codecov/codecov-action@v2
|
||||||
with:
|
with:
|
||||||
files: ${{ github.workspace }}/coverage.txt
|
files: ${{ github.workspace }}/coverage.txt,${{ github.workspace }}/lcov.info
|
||||||
flags: unittests
|
flags: unittests
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
verbose: true
|
verbose: true
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build ${{ matrix.cfg.goos }}/${{ matrix.cfg.goarch }}
|
name: Build ${{ matrix.cfg.goos }}/${{ matrix.cfg.goarch }}
|
||||||
runs-on: ${{ matrix.cfg.on }}
|
runs-on: ${{ matrix.cfg.on }}
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ ifneq ($(OS),Windows_NT)
|
|||||||
./$(WEB_BINARY_NAME) || true
|
./$(WEB_BINARY_NAME) || true
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
########################################################################################################################
|
||||||
|
# Binary
|
||||||
|
########################################################################################################################
|
||||||
|
|
||||||
.PHONY: binary-frontend
|
.PHONY: binary-frontend
|
||||||
# reduce logging, disable angular-cli analytics for ci environment
|
# reduce logging, disable angular-cli analytics for ci environment
|
||||||
binary-frontend: export NPM_CONFIG_LOGLEVEL = warn
|
binary-frontend: export NPM_CONFIG_LOGLEVEL = warn
|
||||||
@@ -100,6 +104,12 @@ binary-frontend:
|
|||||||
npm ci
|
npm ci
|
||||||
npm run build:prod -- --output-path=$(CURDIR)/dist
|
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
|
# Docker
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ func GetDevicesSummary(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//this must match DeviceSummaryWrapper (webapp/backend/pkg/models/device_summary.go)
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"data": map[string]interface{}{
|
"data": map[string]interface{}{
|
||||||
|
|||||||
@@ -46,3 +46,5 @@ testem.log
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
/dist
|
/dist
|
||||||
|
|
||||||
|
/coverage
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ module.exports = function (config)
|
|||||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||||
},
|
},
|
||||||
coverageIstanbulReporter: {
|
coverageIstanbulReporter: {
|
||||||
dir : require('path').join(__dirname, './coverage/treo'),
|
dir: require('path').join(__dirname, './coverage'),
|
||||||
reports : ['html', 'lcovonly', 'text-summary'],
|
reports: ['html', 'lcovonly', 'text-summary'],
|
||||||
fixWebpackSourcePaths: true
|
fixWebpackSourcePaths: true
|
||||||
},
|
},
|
||||||
reporters : ['progress', 'kjhtml'],
|
reporters : ['progress', 'kjhtml'],
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
import { Layout } from 'app/layout/layout.types';
|
import {Layout} from 'app/layout/layout.types';
|
||||||
|
|
||||||
// Theme type
|
// Theme type
|
||||||
export type Theme = 'light' | 'dark' | 'system';
|
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
|
* AppConfig interface. Update this interface to strictly type your config
|
||||||
* object.
|
* object.
|
||||||
*/
|
*/
|
||||||
export interface AppConfig
|
export interface AppConfig {
|
||||||
{
|
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
|
|
||||||
// Dashboard options
|
// Dashboard options
|
||||||
dashboardDisplay: string;
|
dashboardDisplay: DashboardDisplay;
|
||||||
dashboardSort: string;
|
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
|
// maps to webapp/backend/pkg/models/device.go
|
||||||
export interface DeviceModel {
|
export interface DeviceModel {
|
||||||
wwn: string;
|
wwn: string;
|
||||||
device_name: string;
|
device_name?: string;
|
||||||
device_uuid: string;
|
device_uuid?: string;
|
||||||
device_serial_id: string;
|
device_serial_id?: string;
|
||||||
device_label: string;
|
device_label?: string;
|
||||||
|
|
||||||
manufacturer: string;
|
manufacturer: string;
|
||||||
model_name: 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
+57
-18
@@ -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', () => {
|
describe('DashboardDeviceDeleteDialogComponent', () => {
|
||||||
let component: DashboardDeviceDeleteDialogComponent;
|
let component: DashboardDeviceDeleteDialogComponent;
|
||||||
let fixture: ComponentFixture<DashboardDeviceDeleteDialogComponent>;
|
let fixture: ComponentFixture<DashboardDeviceDeleteDialogComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
const matDialogRefSpy = jasmine.createSpyObj('MatDialogRef', ['closeDialog', 'close']);
|
||||||
TestBed.configureTestingModule({
|
const dashboardDeviceDeleteDialogServiceSpy = jasmine.createSpyObj('DashboardDeviceDeleteDialogService', ['deleteDevice']);
|
||||||
declarations: [ DashboardDeviceDeleteDialogComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async(() => {
|
||||||
fixture = TestBed.createComponent(DashboardDeviceDeleteDialogComponent);
|
TestBed.configureTestingModule({
|
||||||
component = fixture.componentInstance;
|
imports: [
|
||||||
fixture.detectChanges();
|
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()
|
||||||
|
}));
|
||||||
|
|
||||||
it('should create', () => {
|
beforeEach(() => {
|
||||||
expect(component).toBeTruthy();
|
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
-2
@@ -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 {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
|
||||||
import {DashboardDeviceDeleteDialogService} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.service';
|
import {DashboardDeviceDeleteDialogService} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.service';
|
||||||
import {Subject} from 'rxjs';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard-device-delete-dialog',
|
selector: 'app-dashboard-device-delete-dialog',
|
||||||
|
|||||||
+7
-30
@@ -1,44 +1,21 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import {NgModule} from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import {RouterModule} from '@angular/router';
|
||||||
import { Overlay } from '@angular/cdk/overlay';
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import {SharedModule} from 'app/shared/shared.module';
|
||||||
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 {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component'
|
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 {dashboardRoutes} from 'app/modules/dashboard/dashboard.routing';
|
||||||
import {MatDividerModule} from '@angular/material/divider';
|
import {MatDialogModule} from '@angular/material/dialog';
|
||||||
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';
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
DashboardDeviceDeleteDialogComponent
|
DashboardDeviceDeleteDialogComponent
|
||||||
],
|
],
|
||||||
imports : [
|
imports: [
|
||||||
RouterModule.forChild([]),
|
RouterModule.forChild([]),
|
||||||
RouterModule.forChild(dashboardRoutes),
|
RouterModule.forChild(dashboardRoutes),
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatDividerModule,
|
|
||||||
MatTooltipModule,
|
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatMenuModule,
|
|
||||||
MatProgressBarModule,
|
|
||||||
MatSortModule,
|
|
||||||
MatTableModule,
|
|
||||||
NgApexchartsModule,
|
|
||||||
SharedModule,
|
SharedModule,
|
||||||
MatDialogModule
|
MatDialogModule
|
||||||
],
|
],
|
||||||
|
|||||||
+98
-18
@@ -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', () => {
|
describe('DashboardDeviceComponent', () => {
|
||||||
let component: DashboardDeviceComponent;
|
let component: DashboardDeviceComponent;
|
||||||
let fixture: ComponentFixture<DashboardDeviceComponent>;
|
let fixture: ComponentFixture<DashboardDeviceComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']);
|
||||||
TestBed.configureTestingModule({
|
// const configServiceSpy = jasmine.createSpyObj('TreoConfigService', ['config$']);
|
||||||
declarations: [ DashboardDeviceComponent ]
|
|
||||||
|
|
||||||
|
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
+20
-17
@@ -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 * as moment from 'moment';
|
||||||
import {takeUntil} from 'rxjs/operators';
|
import {takeUntil} from 'rxjs/operators';
|
||||||
import {AppConfig} from 'app/core/config/app.config';
|
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 {Subject} from 'rxjs';
|
||||||
import humanizeDuration from 'humanize-duration'
|
import humanizeDuration from 'humanize-duration'
|
||||||
import {MatDialog} from '@angular/material/dialog';
|
import {MatDialog} from '@angular/material/dialog';
|
||||||
import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component';
|
import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component';
|
||||||
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
|
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
|
||||||
|
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard-device',
|
selector: 'app-dashboard-device',
|
||||||
templateUrl: './dashboard-device.component.html',
|
templateUrl: './dashboard-device.component.html',
|
||||||
styleUrls: ['./dashboard-device.component.scss']
|
styleUrls: ['./dashboard-device.component.scss']
|
||||||
})
|
})
|
||||||
export class DashboardDeviceComponent implements OnInit {
|
export class DashboardDeviceComponent implements OnInit {
|
||||||
|
|
||||||
@@ -23,7 +24,8 @@ export class DashboardDeviceComponent implements OnInit {
|
|||||||
// Set the private defaults
|
// Set the private defaults
|
||||||
this._unsubscribeAll = new Subject();
|
this._unsubscribeAll = new Subject();
|
||||||
}
|
}
|
||||||
@Input() deviceSummary: any;
|
|
||||||
|
@Input() deviceSummary: DeviceSummaryModel;
|
||||||
@Input() deviceWWN: string;
|
@Input() deviceWWN: string;
|
||||||
@Output() deviceDeleted = new EventEmitter<string>();
|
@Output() deviceDeleted = new EventEmitter<string>();
|
||||||
|
|
||||||
@@ -47,28 +49,27 @@ export class DashboardDeviceComponent implements OnInit {
|
|||||||
// @ Public methods
|
// @ Public methods
|
||||||
// -----------------------------------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
classDeviceLastUpdatedOn(deviceSummary): string {
|
classDeviceLastUpdatedOn(deviceSummary: DeviceSummaryModel): string {
|
||||||
if (deviceSummary.device.device_status !== 0) {
|
if (deviceSummary.device.device_status !== 0) {
|
||||||
return 'text-red' // if the device has failed, always highlight in red
|
return 'text-red' // if the device has failed, always highlight in red
|
||||||
} else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){
|
} else if (deviceSummary.device.device_status === 0 && deviceSummary.smart) {
|
||||||
if(moment().subtract(14, 'd').isBefore(deviceSummary.smart.collector_date)){
|
if (moment().subtract(14, 'days').isBefore(deviceSummary.smart.collector_date)) {
|
||||||
// this device was updated in the last 2 weeks.
|
// this device was updated in the last 2 weeks.
|
||||||
return 'text-green'
|
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
|
// this device was updated in the last month
|
||||||
return 'text-yellow'
|
return 'text-yellow'
|
||||||
} else{
|
} else {
|
||||||
// last updated more than a month ago.
|
// last updated more than a month ago.
|
||||||
return 'text-red'
|
return 'text-red'
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceStatusString(deviceStatus): string {
|
deviceStatusString(deviceStatus: number): string {
|
||||||
if(deviceStatus === 0){
|
if (deviceStatus === 0) {
|
||||||
return 'passed'
|
return 'passed'
|
||||||
} else {
|
} else {
|
||||||
return 'failed'
|
return 'failed'
|
||||||
@@ -76,16 +77,18 @@ export class DashboardDeviceComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
openDeleteDialog(): void {
|
openDeleteDialog(): void {
|
||||||
const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, {
|
const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, {
|
||||||
// width: '250px',
|
// 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 => {
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
console.log('The dialog was closed', result);
|
console.log('The dialog was closed', result);
|
||||||
if(result.success){
|
if (result.success) {
|
||||||
this.deviceDeleted.emit(this.deviceWWN)
|
this.deviceDeleted.emit(this.deviceWWN)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,53 +1,30 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import {NgModule} from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import {RouterModule} from '@angular/router';
|
||||||
import { Overlay } from '@angular/cdk/overlay';
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import {SharedModule} from 'app/shared/shared.module';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
|
||||||
import { MatInputModule } from '@angular/material/input';
|
|
||||||
import { SharedModule } from 'app/shared/shared.module';
|
|
||||||
import {DashboardDeviceComponent} from 'app/layout/common/dashboard-device/dashboard-device.component'
|
import {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 {dashboardRoutes} from '../../../modules/dashboard/dashboard.routing';
|
||||||
import {MatDividerModule} from '@angular/material/divider';
|
|
||||||
import {MatMenuModule} from '@angular/material/menu';
|
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';
|
import {DashboardDeviceDeleteDialogModule} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
DashboardDeviceComponent
|
DashboardDeviceComponent
|
||||||
],
|
],
|
||||||
imports : [
|
imports: [
|
||||||
RouterModule.forChild([]),
|
RouterModule.forChild([]),
|
||||||
RouterModule.forChild(dashboardRoutes),
|
RouterModule.forChild(dashboardRoutes),
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatDividerModule,
|
|
||||||
MatTooltipModule,
|
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
MatProgressBarModule,
|
|
||||||
MatSortModule,
|
|
||||||
MatTableModule,
|
|
||||||
NgApexchartsModule,
|
|
||||||
SharedModule,
|
SharedModule,
|
||||||
DashboardDeviceDeleteDialogModule
|
DashboardDeviceDeleteDialogModule
|
||||||
],
|
],
|
||||||
exports : [
|
exports: [
|
||||||
DashboardDeviceComponent,
|
DashboardDeviceComponent,
|
||||||
],
|
],
|
||||||
providers : []
|
providers: []
|
||||||
})
|
})
|
||||||
export class DashboardDeviceModule
|
export class DashboardDeviceModule {
|
||||||
{
|
|
||||||
}
|
}
|
||||||
|
|||||||
-25
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+19
-21
@@ -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 {AppConfig} from 'app/core/config/app.config';
|
||||||
import { TreoConfigService } from '@treo/services/config';
|
import {TreoConfigService} from '@treo/services/config';
|
||||||
import {Subject} from 'rxjs';
|
import {Subject} from 'rxjs';
|
||||||
import {takeUntil} from 'rxjs/operators';
|
import {takeUntil} from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard-settings',
|
selector: 'app-dashboard-settings',
|
||||||
templateUrl: './dashboard-settings.component.html',
|
templateUrl: './dashboard-settings.component.html',
|
||||||
styleUrls: ['./dashboard-settings.component.scss']
|
styleUrls: ['./dashboard-settings.component.scss']
|
||||||
})
|
})
|
||||||
export class DashboardSettingsComponent implements OnInit {
|
export class DashboardSettingsComponent implements OnInit {
|
||||||
|
|
||||||
@@ -26,25 +26,23 @@ export class DashboardSettingsComponent implements OnInit {
|
|||||||
this._unsubscribeAll = new Subject();
|
this._unsubscribeAll = new Subject();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
// Subscribe to config changes
|
// Subscribe to config changes
|
||||||
this._configService.config$
|
this._configService.config$
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
.pipe(takeUntil(this._unsubscribeAll))
|
||||||
.subscribe((config: AppConfig) => {
|
.subscribe((config: AppConfig) => {
|
||||||
|
|
||||||
// Store the config
|
// Store the config
|
||||||
this.dashboardDisplay = config.dashboardDisplay;
|
this.dashboardDisplay = config.dashboardDisplay;
|
||||||
this.dashboardSort = config.dashboardSort;
|
this.dashboardSort = config.dashboardSort;
|
||||||
this.temperatureUnit = config.temperatureUnit;
|
this.temperatureUnit = config.temperatureUnit;
|
||||||
this.theme = config.theme;
|
this.theme = config.theme;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSettings(): void {
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSettings(): void {
|
||||||
const newSettings = {
|
const newSettings = {
|
||||||
dashboardDisplay: this.dashboardDisplay,
|
dashboardDisplay: this.dashboardDisplay,
|
||||||
dashboardSort: this.dashboardSort,
|
dashboardSort: this.dashboardSort,
|
||||||
@@ -53,7 +51,7 @@ export class DashboardSettingsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
this._configService.config = newSettings
|
this._configService.config = newSettings
|
||||||
console.log(`Saved Settings: ${JSON.stringify(newSettings)}`)
|
console.log(`Saved Settings: ${JSON.stringify(newSettings)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
formatLabel(value: number): number {
|
formatLabel(value: number): number {
|
||||||
return value;
|
return value;
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import {NgModule} from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import {RouterModule} from '@angular/router';
|
||||||
import { Overlay } from '@angular/cdk/overlay';
|
import {MatAutocompleteModule} from '@angular/material/autocomplete';
|
||||||
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import {MatSelectModule} from '@angular/material/select';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import {MatInputModule} from '@angular/material/input';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import {SharedModule} from 'app/shared/shared.module';
|
||||||
import { SharedModule } from 'app/shared/shared.module';
|
|
||||||
import {DetailSettingsComponent} from 'app/layout/common/detail-settings/detail-settings.component'
|
import {DetailSettingsComponent} from 'app/layout/common/detail-settings/detail-settings.component'
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import {MatDialogModule} from '@angular/material/dialog';
|
||||||
import { MatButtonToggleModule} from '@angular/material/button-toggle';
|
import {MatButtonToggleModule} from '@angular/material/button-toggle';
|
||||||
import {MatTabsModule} from '@angular/material/tabs';
|
import {MatTabsModule} from '@angular/material/tabs';
|
||||||
import {MatSliderModule} from '@angular/material/slider';
|
import {MatSliderModule} from '@angular/material/slider';
|
||||||
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
|
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
|
||||||
@@ -20,7 +19,7 @@ import {MatTooltipModule} from '@angular/material/tooltip';
|
|||||||
declarations: [
|
declarations: [
|
||||||
DetailSettingsComponent
|
DetailSettingsComponent
|
||||||
],
|
],
|
||||||
imports : [
|
imports: [
|
||||||
RouterModule.forChild([]),
|
RouterModule.forChild([]),
|
||||||
MatAutocompleteModule,
|
MatAutocompleteModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
@@ -36,11 +35,10 @@ import {MatTooltipModule} from '@angular/material/tooltip';
|
|||||||
MatSlideToggleModule,
|
MatSlideToggleModule,
|
||||||
SharedModule
|
SharedModule
|
||||||
],
|
],
|
||||||
exports : [
|
exports: [
|
||||||
DetailSettingsComponent
|
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-col flex-auto w-full p-8 xs:p-2">
|
||||||
|
|
||||||
<div class="flex flex-wrap w-full">
|
<div class="flex flex-wrap w-full">
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
import {
|
||||||
import { MatSort } from '@angular/material/sort';
|
AfterViewInit,
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
ChangeDetectionStrategy,
|
||||||
import { Subject } from 'rxjs';
|
Component,
|
||||||
import { takeUntil } from 'rxjs/operators';
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
ViewChild,
|
||||||
|
ViewEncapsulation
|
||||||
|
} from '@angular/core';
|
||||||
|
import {Subject} from 'rxjs';
|
||||||
|
import {takeUntil} from 'rxjs/operators';
|
||||||
import {ApexOptions, ChartComponent} from 'ng-apexcharts';
|
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 {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 {AppConfig} from 'app/core/config/app.config';
|
||||||
import {TreoConfigService} from '@treo/services/config';
|
import {TreoConfigService} from '@treo/services/config';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import {TemperaturePipe} from 'app/shared/temperature.pipe';
|
import {TemperaturePipe} from 'app/shared/temperature.pipe';
|
||||||
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
|
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
|
||||||
|
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector : 'example',
|
selector : 'example',
|
||||||
@@ -22,7 +29,7 @@ import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
|
|||||||
})
|
})
|
||||||
export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||||
{
|
{
|
||||||
data: any;
|
summaryData: { [key: string]: DeviceSummaryModel };
|
||||||
hostGroups: { [hostId: string]: string[] } = {}
|
hostGroups: { [hostId: string]: string[] } = {}
|
||||||
temperatureOptions: ApexOptions;
|
temperatureOptions: ApexOptions;
|
||||||
tempDurationKey = 'forever'
|
tempDurationKey = 'forever'
|
||||||
@@ -35,10 +42,13 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
|||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
* @param {SmartService} _smartService
|
* @param {DashboardService} _dashboardService
|
||||||
|
* @param {TreoConfigService} _configService
|
||||||
|
* @param {MatDialog} dialog
|
||||||
|
* @param {Router} router
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private _smartService: DashboardService,
|
private _dashboardService: DashboardService,
|
||||||
private _configService: TreoConfigService,
|
private _configService: TreoConfigService,
|
||||||
public dialog: MatDialog,
|
public dialog: MatDialog,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@@ -81,16 +91,16 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get the data
|
// Get the data
|
||||||
this._smartService.data$
|
this._dashboardService.data$
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
.pipe(takeUntil(this._unsubscribeAll))
|
||||||
.subscribe((data) => {
|
.subscribe((data) => {
|
||||||
|
|
||||||
// Store the data
|
// Store the data
|
||||||
this.data = data;
|
this.summaryData = data;
|
||||||
|
|
||||||
// generate group data.
|
// generate group data.
|
||||||
for(const wwn in this.data.data.summary){
|
for (const wwn in this.summaryData) {
|
||||||
const hostid = this.data.data.summary[wwn].device.host_id
|
const hostid = this.summaryData[wwn].device.host_id
|
||||||
const hostDeviceList = this.hostGroups[hostid] || []
|
const hostDeviceList = this.hostGroups[hostid] || []
|
||||||
hostDeviceList.push(wwn)
|
hostDeviceList.push(wwn)
|
||||||
this.hostGroups[hostid] = hostDeviceList
|
this.hostGroups[hostid] = hostDeviceList
|
||||||
@@ -132,11 +142,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
|||||||
private _deviceDataTemperatureSeries(): any[] {
|
private _deviceDataTemperatureSeries(): any[] {
|
||||||
const deviceTemperatureSeries = []
|
const deviceTemperatureSeries = []
|
||||||
|
|
||||||
console.log('DEVICE DATA SUMMARY', this.data)
|
console.log('DEVICE DATA SUMMARY', this.summaryData)
|
||||||
|
|
||||||
for(const wwn in this.data.data.summary){
|
for (const wwn in this.summaryData) {
|
||||||
const deviceSummary = this.data.data.summary[wwn]
|
const deviceSummary = this.summaryData[wwn]
|
||||||
if (!deviceSummary.temp_history){
|
if (!deviceSummary.temp_history) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +216,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
xaxis : {
|
xaxis: {
|
||||||
type: 'datetime'
|
type: 'datetime'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -216,11 +226,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
|||||||
// @ Public methods
|
// @ Public methods
|
||||||
// -----------------------------------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
deviceSummariesForHostGroup(hostGroupWWNs: string[]): any[] {
|
deviceSummariesForHostGroup(hostGroupWWNs: string[]): DeviceSummaryModel[] {
|
||||||
const deviceSummaries = []
|
const deviceSummaries: DeviceSummaryModel[] = []
|
||||||
for(const wwn of hostGroupWWNs){
|
for (const wwn of hostGroupWWNs) {
|
||||||
if(this.data.data.summary[wwn]){
|
if (this.summaryData[wwn]) {
|
||||||
deviceSummaries.push(this.data.data.summary[wwn])
|
deviceSummaries.push(this.summaryData[wwn])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return deviceSummaries
|
return deviceSummaries
|
||||||
@@ -235,7 +245,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDeviceDeleted(wwn: string): void {
|
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"
|
DURATION_KEY_FOREVER = "forever"
|
||||||
*/
|
*/
|
||||||
|
|
||||||
changeSummaryTempDuration(durationKey: string){
|
changeSummaryTempDuration(durationKey: string): void {
|
||||||
this.tempDurationKey = durationKey
|
this.tempDurationKey = durationKey
|
||||||
|
|
||||||
this._smartService.getSummaryTempData(durationKey)
|
this._dashboardService.getSummaryTempData(durationKey)
|
||||||
.subscribe((data) => {
|
.subscribe((tempHistoryData) => {
|
||||||
|
|
||||||
// given a list of device temp history, override the data in the "summary" object.
|
// 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}`)
|
// 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
|
// Prepare the chart series data
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import {Observable} from 'rxjs';
|
||||||
import { DashboardService } from 'app/modules/dashboard/dashboard.service';
|
import {DashboardService} from 'app/modules/dashboard/dashboard.service';
|
||||||
|
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class DashboardResolver implements Resolve<any>
|
export class DashboardResolver implements Resolve<any> {
|
||||||
{
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
@@ -29,8 +29,7 @@ export class DashboardResolver implements Resolve<any>
|
|||||||
* @param route
|
* @param route
|
||||||
* @param state
|
* @param state
|
||||||
*/
|
*/
|
||||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any>
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<{ [p: string]: DeviceSummaryModel }> {
|
||||||
{
|
|
||||||
return this._dashboardService.getSummaryData();
|
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 {Injectable} from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import {BehaviorSubject, Observable} from 'rxjs';
|
||||||
import { tap } from 'rxjs/operators';
|
import {map, tap} from 'rxjs/operators';
|
||||||
import { getBasePath } from 'app/app.routing';
|
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({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class DashboardService
|
export class DashboardService {
|
||||||
{
|
|
||||||
// Observables
|
// Observables
|
||||||
private _data: BehaviorSubject<any>;
|
private _data: BehaviorSubject<{ [p: string]: DeviceSummaryModel }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
@@ -32,8 +35,7 @@ export class DashboardService
|
|||||||
/**
|
/**
|
||||||
* Getter for data
|
* Getter for data
|
||||||
*/
|
*/
|
||||||
get data$(): Observable<any>
|
get data$(): Observable<{ [p: string]: DeviceSummaryModel }> {
|
||||||
{
|
|
||||||
return this._data.asObservable();
|
return this._data.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,22 +46,28 @@ export class DashboardService
|
|||||||
/**
|
/**
|
||||||
* Get data
|
* Get data
|
||||||
*/
|
*/
|
||||||
getSummaryData(): Observable<any>
|
getSummaryData(): Observable<{ [key: string]: DeviceSummaryModel }> {
|
||||||
{
|
|
||||||
return this._httpClient.get(getBasePath() + '/api/summary').pipe(
|
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);
|
this._data.next(response);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSummaryTempData(durationKey: string): Observable<any>
|
getSummaryTempData(durationKey: string): Observable<{ [key: string]: SmartTemperatureModel[] }> {
|
||||||
{
|
|
||||||
const params = {}
|
const params = {}
|
||||||
if(durationKey){
|
if (durationKey) {
|
||||||
params['duration_key'] = 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 {ApexOptions} from 'ng-apexcharts';
|
||||||
import {MatTableDataSource} from '@angular/material/table';
|
import {AppConfig} from 'app/core/config/app.config';
|
||||||
import {MatSort} from '@angular/material/sort';
|
|
||||||
import {Subject} from 'rxjs';
|
|
||||||
import {DetailService} from './detail.service';
|
import {DetailService} from './detail.service';
|
||||||
import {takeUntil} from 'rxjs/operators';
|
|
||||||
import {DetailSettingsComponent} from 'app/layout/common/detail-settings/detail-settings.component';
|
import {DetailSettingsComponent} from 'app/layout/common/detail-settings/detail-settings.component';
|
||||||
import {MatDialog} from '@angular/material/dialog';
|
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 {TreoConfigService} from '@treo/services/config';
|
||||||
import {AppConfig} from 'app/core/config/app.config';
|
|
||||||
import {animate, state, style, transition, trigger} from '@angular/animations';
|
import {animate, state, style, transition, trigger} from '@angular/animations';
|
||||||
import {formatDate} from '@angular/common';
|
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
|
// from Constants.go - these must match
|
||||||
const AttributeStatusPassed = 0
|
const AttributeStatusPassed = 0
|
||||||
@@ -22,9 +25,9 @@ const AttributeStatusFailedScrutiny = 4
|
|||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'detail',
|
selector: 'detail',
|
||||||
templateUrl: './detail.component.html',
|
templateUrl: './detail.component.html',
|
||||||
styleUrls: ['./detail.component.scss'],
|
styleUrls: ['./detail.component.scss'],
|
||||||
animations: [
|
animations: [
|
||||||
trigger('detailExpand', [
|
trigger('detailExpand', [
|
||||||
state('collapsed', style({height: '0px', minHeight: '0'})),
|
state('collapsed', style({height: '0px', minHeight: '0'})),
|
||||||
@@ -40,22 +43,23 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
* @param {DetailService} _detailService
|
* @param {DetailService} _detailService
|
||||||
|
* @param {MatDialog} dialog
|
||||||
|
* @param {TreoConfigService} _configService
|
||||||
|
* @param {string} locale
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private _detailService: DetailService,
|
private _detailService: DetailService,
|
||||||
public dialog: MatDialog,
|
public dialog: MatDialog,
|
||||||
private _configService: TreoConfigService,
|
private _configService: TreoConfigService,
|
||||||
@Inject(LOCALE_ID) public locale: string
|
@Inject(LOCALE_ID) public locale: string
|
||||||
|
) {
|
||||||
)
|
|
||||||
{
|
|
||||||
// Set the private defaults
|
// Set the private defaults
|
||||||
this._unsubscribeAll = new Subject();
|
this._unsubscribeAll = new Subject();
|
||||||
|
|
||||||
// Set the defaults
|
// Set the defaults
|
||||||
this.smartAttributeDataSource = new MatTableDataSource();
|
this.smartAttributeDataSource = new MatTableDataSource();
|
||||||
// this.recentTransactionsTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh'];
|
// 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;
|
this.systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
@@ -65,14 +69,15 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
onlyCritical = true;
|
onlyCritical = true;
|
||||||
// data: any;
|
// data: any;
|
||||||
expandedAttribute: any | null;
|
expandedAttribute: SmartAttributeModel | null;
|
||||||
|
|
||||||
metadata: any;
|
metadata: { [p: string]: AttributeMetadataModel } | { [p: number]: AttributeMetadataModel };
|
||||||
device: any;
|
device: DeviceModel;
|
||||||
smart_results: any[];
|
// tslint:disable-next-line:variable-name
|
||||||
|
smart_results: SmartModel[];
|
||||||
|
|
||||||
commonSparklineOptions: Partial<ApexOptions>;
|
commonSparklineOptions: Partial<ApexOptions>;
|
||||||
smartAttributeDataSource: MatTableDataSource<any>;
|
smartAttributeDataSource: MatTableDataSource<SmartAttributeModel>;
|
||||||
smartAttributeTableColumns: string[];
|
smartAttributeTableColumns: string[];
|
||||||
|
|
||||||
@ViewChild('smartAttributeTable', {read: MatSort})
|
@ViewChild('smartAttributeTable', {read: MatSort})
|
||||||
@@ -91,8 +96,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* On init
|
* On init
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void
|
ngOnInit(): void {
|
||||||
{
|
|
||||||
// Subscribe to config changes
|
// Subscribe to config changes
|
||||||
this._configService.config$
|
this._configService.config$
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
.pipe(takeUntil(this._unsubscribeAll))
|
||||||
@@ -104,13 +108,13 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
// Get the data
|
// Get the data
|
||||||
this._detailService.data$
|
this._detailService.data$
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
.pipe(takeUntil(this._unsubscribeAll))
|
||||||
.subscribe((data) => {
|
.subscribe((respWrapper) => {
|
||||||
|
|
||||||
// Store the data
|
// Store the data
|
||||||
// this.data = data;
|
// this.data = data;
|
||||||
this.device = data.data.device;
|
this.device = respWrapper.data.device;
|
||||||
this.smart_results = data.data.smart_results
|
this.smart_results = respWrapper.data.smart_results
|
||||||
this.metadata = data.metadata;
|
this.metadata = respWrapper.metadata;
|
||||||
|
|
||||||
|
|
||||||
// Store the table data
|
// Store the table data
|
||||||
@@ -124,8 +128,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* After view init
|
* After view init
|
||||||
*/
|
*/
|
||||||
ngAfterViewInit(): void
|
ngAfterViewInit(): void {
|
||||||
{
|
|
||||||
// Make the data source sortable
|
// Make the data source sortable
|
||||||
this.smartAttributeDataSource.sort = this.smartAttributeTableMatSort;
|
this.smartAttributeDataSource.sort = this.smartAttributeTableMatSort;
|
||||||
}
|
}
|
||||||
@@ -133,8 +136,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* On destroy
|
* On destroy
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void
|
ngOnDestroy(): void {
|
||||||
{
|
|
||||||
// Unsubscribe from all subscriptions
|
// Unsubscribe from all subscriptions
|
||||||
this._unsubscribeAll.next();
|
this._unsubscribeAll.next();
|
||||||
this._unsubscribeAll.complete();
|
this._unsubscribeAll.complete();
|
||||||
@@ -147,22 +149,23 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
getAttributeStatusName(attributeStatus: number): string {
|
getAttributeStatusName(attributeStatus: number): string {
|
||||||
// tslint:disable:no-bitwise
|
// tslint:disable:no-bitwise
|
||||||
|
|
||||||
if(attributeStatus === AttributeStatusPassed){
|
if (attributeStatus === AttributeStatusPassed) {
|
||||||
return 'passed'
|
return 'passed'
|
||||||
|
|
||||||
} else if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0 || (attributeStatus & AttributeStatusFailedSmart) !== 0 ){
|
} else if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0 || (attributeStatus & AttributeStatusFailedSmart) !== 0) {
|
||||||
return 'failed'
|
return 'failed'
|
||||||
} else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0){
|
} else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0) {
|
||||||
return 'warn'
|
return 'warn'
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
// tslint:enable:no-bitwise
|
// tslint:enable:no-bitwise
|
||||||
}
|
}
|
||||||
|
|
||||||
getAttributeScrutinyStatusName(attributeStatus: number): string {
|
getAttributeScrutinyStatusName(attributeStatus: number): string {
|
||||||
// tslint:disable:no-bitwise
|
// tslint:disable:no-bitwise
|
||||||
if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0){
|
if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0) {
|
||||||
return 'failed'
|
return 'failed'
|
||||||
} else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0){
|
} else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0) {
|
||||||
return 'warn'
|
return 'warn'
|
||||||
} else {
|
} else {
|
||||||
return 'passed'
|
return 'passed'
|
||||||
@@ -172,7 +175,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
getAttributeSmartStatusName(attributeStatus: number): string {
|
getAttributeSmartStatusName(attributeStatus: number): string {
|
||||||
// tslint:disable:no-bitwise
|
// tslint:disable:no-bitwise
|
||||||
if ((attributeStatus & AttributeStatusFailedSmart) !== 0){
|
if ((attributeStatus & AttributeStatusFailedSmart) !== 0) {
|
||||||
return 'failed'
|
return 'failed'
|
||||||
} else {
|
} else {
|
||||||
return 'passed'
|
return 'passed'
|
||||||
@@ -181,138 +184,140 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getAttributeName(attribute_data): string {
|
getAttributeName(attributeData: SmartAttributeModel): string {
|
||||||
const attribute_metadata = this.metadata[attribute_data.attribute_id]
|
const attributeMetadata = this.metadata[attributeData.attribute_id]
|
||||||
if(!attribute_metadata){
|
if (!attributeMetadata) {
|
||||||
return 'Unknown Attribute Name'
|
return 'Unknown Attribute Name'
|
||||||
} else {
|
} else {
|
||||||
return attribute_metadata.display_name
|
return attributeMetadata.display_name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getAttributeDescription(attribute_data){
|
|
||||||
const attribute_metadata = this.metadata[attribute_data.attribute_id]
|
getAttributeDescription(attributeData: SmartAttributeModel): string {
|
||||||
if(!attribute_metadata){
|
const attributeMetadata = this.metadata[attributeData.attribute_id]
|
||||||
|
if (!attributeMetadata) {
|
||||||
return 'Unknown'
|
return 'Unknown'
|
||||||
} else {
|
} else {
|
||||||
return attribute_metadata.description
|
return attributeMetadata.description
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAttributeValue(attribute_data){
|
getAttributeValue(attributeData: SmartAttributeModel): number {
|
||||||
if(this.isAta()) {
|
if (this.isAta()) {
|
||||||
const attribute_metadata = this.metadata[attribute_data.attribute_id]
|
const attributeMetadata = this.metadata[attributeData.attribute_id]
|
||||||
if(!attribute_metadata){
|
if (!attributeMetadata) {
|
||||||
return attribute_data.value
|
return attributeData.value
|
||||||
} else if (attribute_metadata.display_type == 'raw') {
|
} else if (attributeMetadata.display_type === 'raw') {
|
||||||
return attribute_data.raw_value
|
return attributeData.raw_value
|
||||||
} else if (attribute_metadata.display_type == 'transformed' && attribute_data.transformed_value) {
|
} else if (attributeMetadata.display_type === 'transformed' && attributeData.transformed_value) {
|
||||||
return attribute_data.transformed_value
|
return attributeData.transformed_value
|
||||||
} else {
|
} else {
|
||||||
return attribute_data.value
|
return attributeData.value
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else{
|
return attributeData.value
|
||||||
return attribute_data.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAttributeValueType(attribute_data){
|
getAttributeValueType(attributeData: SmartAttributeModel): string {
|
||||||
if(this.isAta()) {
|
if (this.isAta()) {
|
||||||
const attribute_metadata = this.metadata[attribute_data.attribute_id]
|
const attributeMetadata = this.metadata[attributeData.attribute_id]
|
||||||
if(!attribute_metadata){
|
if (!attributeMetadata) {
|
||||||
return ''
|
return ''
|
||||||
} else {
|
} else {
|
||||||
return attribute_metadata.display_type
|
return attributeMetadata.display_type
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAttributeIdeal(attribute_data){
|
getAttributeIdeal(attributeData: SmartAttributeModel): string {
|
||||||
if(this.isAta()){
|
if (this.isAta()) {
|
||||||
return this.metadata[attribute_data.attribute_id]?.display_type == 'raw' ? this.metadata[attribute_data.attribute_id]?.ideal : ''
|
return this.metadata[attributeData.attribute_id]?.display_type === 'raw' ? this.metadata[attributeData.attribute_id]?.ideal : ''
|
||||||
} else {
|
} else {
|
||||||
return this.metadata[attribute_data.attribute_id]?.ideal
|
return this.metadata[attributeData.attribute_id]?.ideal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAttributeWorst(attribute_data){
|
getAttributeWorst(attributeData: SmartAttributeModel): number | string {
|
||||||
const attribute_metadata = this.metadata[attribute_data.attribute_id]
|
const attributeMetadata = this.metadata[attributeData.attribute_id]
|
||||||
if(!attribute_metadata){
|
if (!attributeMetadata) {
|
||||||
return attribute_data.worst
|
return attributeData.worst
|
||||||
} else {
|
} else {
|
||||||
return attribute_metadata?.display_type == 'normalized' ? attribute_data.worst : ''
|
return attributeMetadata?.display_type === 'normalized' ? attributeData.worst : ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAttributeThreshold(attribute_data){
|
getAttributeThreshold(attributeData: SmartAttributeModel): number | string {
|
||||||
if(this.isAta()){
|
if (this.isAta()) {
|
||||||
const attribute_metadata = this.metadata[attribute_data.attribute_id]
|
const attributeMetadata = this.metadata[attributeData.attribute_id]
|
||||||
if(!attribute_metadata || attribute_metadata.display_type == 'normalized'){
|
if (!attributeMetadata || attributeMetadata.display_type === 'normalized') {
|
||||||
return attribute_data.thresh
|
return attributeData.thresh
|
||||||
} else {
|
} else {
|
||||||
// if(this.data.metadata[attribute_data.attribute_id].observed_thresholds){
|
// if(this.data.metadata[attribute_data.attribute_id].observed_thresholds){
|
||||||
//
|
//
|
||||||
// } else {
|
// } else {
|
||||||
// }
|
// }
|
||||||
// return ''
|
// return ''
|
||||||
return attribute_data.thresh
|
return attributeData.thresh
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return (attribute_data.thresh == -1 ? '' : attribute_data.thresh )
|
return (attributeData.thresh === -1 ? '' : attributeData.thresh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAttributeCritical(attribute_data){
|
getAttributeCritical(attributeData: SmartAttributeModel): boolean {
|
||||||
return this.metadata[attribute_data.attribute_id]?.critical
|
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
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
let attributes_length = 0
|
let attributesLength = 0
|
||||||
const attributes = this.smart_results[0]?.attrs
|
const attributes = this.smart_results[0]?.attrs
|
||||||
if (attributes) {
|
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 {
|
isAta(): boolean {
|
||||||
return this.device.device_protocol == 'ATA'
|
return this.device.device_protocol === 'ATA'
|
||||||
}
|
}
|
||||||
|
|
||||||
isScsi(): boolean {
|
isScsi(): boolean {
|
||||||
return this.device.device_protocol == 'SCSI'
|
return this.device.device_protocol === 'SCSI'
|
||||||
}
|
}
|
||||||
|
|
||||||
isNvme(): boolean {
|
isNvme(): boolean {
|
||||||
return this.device.device_protocol == 'NVMe'
|
return this.device.device_protocol === 'NVMe'
|
||||||
}
|
}
|
||||||
|
|
||||||
private _generateSmartAttributeTableDataSource(smart_results){
|
private _generateSmartAttributeTableDataSource(smartResults: SmartModel[]): SmartAttributeModel[] {
|
||||||
const smartAttributeDataSource = [];
|
const smartAttributeDataSource: SmartAttributeModel[] = [];
|
||||||
|
|
||||||
if(smart_results.length == 0){
|
if (smartResults.length === 0) {
|
||||||
return smartAttributeDataSource
|
return smartAttributeDataSource
|
||||||
}
|
}
|
||||||
const latest_smart_result = smart_results[0];
|
const latestSmartResult = smartResults[0];
|
||||||
let attributes = {}
|
let attributes: { [p: string]: SmartAttributeModel } = {}
|
||||||
if(this.isScsi()) {
|
if (this.isScsi()) {
|
||||||
this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'history'];
|
this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'history'];
|
||||||
attributes = latest_smart_result.attrs
|
attributes = latestSmartResult.attrs
|
||||||
} else if(this.isNvme()){
|
} else if (this.isNvme()) {
|
||||||
this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'ideal', 'history'];
|
this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'ideal', 'history'];
|
||||||
attributes = latest_smart_result.attrs
|
attributes = latestSmartResult.attrs
|
||||||
} else {
|
} else {
|
||||||
// ATA
|
// ATA
|
||||||
attributes = latest_smart_result.attrs
|
attributes = latestSmartResult.attrs
|
||||||
this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'thresh','ideal', 'failure', 'history'];
|
this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'thresh', 'ideal', 'failure', 'history'];
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const attrId in attributes){
|
for (const attrId in attributes) {
|
||||||
const attr = attributes[attrId]
|
const attr = attributes[attrId]
|
||||||
|
|
||||||
// chart history data
|
// chart history data
|
||||||
@@ -320,18 +325,18 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
|
|
||||||
const attrHistory = []
|
const attrHistory = []
|
||||||
for (const smart_result of smart_results){
|
for (const smartResult of smartResults) {
|
||||||
// attrHistory.push(this.getAttributeValue(smart_result.attrs[attrId]))
|
// attrHistory.push(this.getAttributeValue(smart_result.attrs[attrId]))
|
||||||
|
|
||||||
const chartDatapoint = {
|
const chartDatapoint = {
|
||||||
x: formatDate(smart_result.date, 'MMMM dd, yyyy - HH:mm', this.locale),
|
x: formatDate(smartResult.date, 'MMMM dd, yyyy - HH:mm', this.locale),
|
||||||
y: this.getAttributeValue(smart_result.attrs[attrId])
|
y: this.getAttributeValue(smartResult.attrs[attrId])
|
||||||
}
|
}
|
||||||
const attributeStatusName = this.getAttributeStatusName(smart_result.attrs[attrId].status)
|
const attributeStatusName = this.getAttributeStatusName(smartResult.attrs[attrId].status)
|
||||||
if(attributeStatusName === 'failed') {
|
if (attributeStatusName === 'failed') {
|
||||||
chartDatapoint['strokeColor'] = '#F05252'
|
chartDatapoint['strokeColor'] = '#F05252'
|
||||||
chartDatapoint['fillColor'] = '#F05252'
|
chartDatapoint['fillColor'] = '#F05252'
|
||||||
} else if (attributeStatusName === 'warn'){
|
} else if (attributeStatusName === 'warn') {
|
||||||
chartDatapoint['strokeColor'] = '#C27803'
|
chartDatapoint['strokeColor'] = '#C27803'
|
||||||
chartDatapoint['fillColor'] = '#C27803'
|
chartDatapoint['fillColor'] = '#C27803'
|
||||||
}
|
}
|
||||||
@@ -350,7 +355,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
// determine when to include the attributes in table.
|
// 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)
|
smartAttributeDataSource.push(attr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -362,8 +367,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private _prepareChartData(): void
|
private _prepareChartData(): void {
|
||||||
{
|
|
||||||
|
|
||||||
// Account balance
|
// Account balance
|
||||||
this.commonSparklineOptions = {
|
this.commonSparklineOptions = {
|
||||||
@@ -392,7 +396,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
title: {
|
title: {
|
||||||
formatter: function(seriesName) {
|
formatter: (seriesName) => {
|
||||||
return '';
|
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') {
|
if (config.theme === 'system') {
|
||||||
return this.systemPrefersDark ? 'dark' : 'light'
|
return this.systemPrefersDark ? 'dark' : 'light'
|
||||||
} else {
|
} else {
|
||||||
return config.theme
|
return config.theme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------------------------------
|
||||||
// @ Public methods
|
// @ Public methods
|
||||||
// -----------------------------------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
toHex(decimalNumb){
|
toHex(decimalNumb: number | string): string {
|
||||||
return '0x' + Number(decimalNumb).toString(16).padStart(2, '0').toUpperCase()
|
return '0x' + Number(decimalNumb).toString(16).padStart(2, '0').toUpperCase()
|
||||||
}
|
}
|
||||||
toggleOnlyCritical(){
|
|
||||||
|
toggleOnlyCritical(): void {
|
||||||
this.onlyCritical = !this.onlyCritical
|
this.onlyCritical = !this.onlyCritical
|
||||||
this.smartAttributeDataSource.data = this._generateSmartAttributeTableDataSource(this.smart_results);
|
this.smartAttributeDataSource.data = this._generateSmartAttributeTableDataSource(this.smart_results);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
openDialog() {
|
openDialog(): void {
|
||||||
const dialogRef = this.dialog.open(DetailSettingsComponent);
|
const dialogRef = this.dialog.open(DetailSettingsComponent);
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe(result => {
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
@@ -444,8 +449,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
* @param index
|
* @param index
|
||||||
* @param item
|
* @param item
|
||||||
*/
|
*/
|
||||||
trackByFn(index: number, item: any): any
|
trackByFn(index: number, item: any): any {
|
||||||
{
|
|
||||||
return index;
|
return index;
|
||||||
// return item.id || index;
|
// return item.id || index;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import {Observable} from 'rxjs';
|
||||||
import { DetailService } from 'app/modules/detail/detail.service';
|
import {DetailService} from 'app/modules/detail/detail.service';
|
||||||
|
import {DeviceDetailsResponseWrapper} from 'app/core/models/device-details-response-wrapper';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class DetailResolver implements Resolve<any>
|
export class DetailResolver implements Resolve<any> {
|
||||||
{
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
@@ -29,8 +29,7 @@ export class DetailResolver implements Resolve<any>
|
|||||||
* @param route
|
* @param route
|
||||||
* @param state
|
* @param state
|
||||||
*/
|
*/
|
||||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any>
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<DeviceDetailsResponseWrapper> {
|
||||||
{
|
|
||||||
return this._detailService.getData(route.params.wwn);
|
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 {Injectable} from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import {BehaviorSubject, Observable} from 'rxjs';
|
||||||
import { tap } from 'rxjs/operators';
|
import {tap} from 'rxjs/operators';
|
||||||
import { getBasePath } from 'app/app.routing';
|
import {getBasePath} from 'app/app.routing';
|
||||||
|
import {DeviceDetailsResponseWrapper} from 'app/core/models/device-details-response-wrapper';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class DetailService
|
export class DetailService {
|
||||||
{
|
|
||||||
// Observables
|
// Observables
|
||||||
private _data: BehaviorSubject<any>;
|
private _data: BehaviorSubject<DeviceDetailsResponseWrapper>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
@@ -19,8 +19,7 @@ export class DetailService
|
|||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private _httpClient: HttpClient
|
private _httpClient: HttpClient
|
||||||
)
|
) {
|
||||||
{
|
|
||||||
// Set the private defaults
|
// Set the private defaults
|
||||||
this._data = new BehaviorSubject(null);
|
this._data = new BehaviorSubject(null);
|
||||||
}
|
}
|
||||||
@@ -32,8 +31,7 @@ export class DetailService
|
|||||||
/**
|
/**
|
||||||
* Getter for data
|
* Getter for data
|
||||||
*/
|
*/
|
||||||
get data$(): Observable<any>
|
get data$(): Observable<DeviceDetailsResponseWrapper> {
|
||||||
{
|
|
||||||
return this._data.asObservable();
|
return this._data.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,10 +42,9 @@ export class DetailService
|
|||||||
/**
|
/**
|
||||||
* Get data
|
* Get data
|
||||||
*/
|
*/
|
||||||
getData(wwn): Observable<any>
|
getData(wwn): Observable<DeviceDetailsResponseWrapper> {
|
||||||
{
|
|
||||||
return this._httpClient.get(getBasePath() + `/api/device/${wwn}/details`).pipe(
|
return this._httpClient.get(getBasePath() + `/api/device/${wwn}/details`).pipe(
|
||||||
tap((response: any) => {
|
tap((response: DeviceDetailsResponseWrapper) => {
|
||||||
this._data.next(response);
|
this._data.next(response);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { DeviceTitlePipe } from './device-title.pipe';
|
import {DeviceTitlePipe} from './device-title.pipe';
|
||||||
import {FileSizePipe} from "./file-size.pipe";
|
import {DeviceModel} from 'app/core/models/device-model';
|
||||||
import {DeviceModel} from "../core/models/device-model";
|
|
||||||
|
|
||||||
describe('DeviceTitlePipe', () => {
|
describe('DeviceTitlePipe', () => {
|
||||||
it('create an instance', () => {
|
it('create an instance', () => {
|
||||||
const pipe = new DeviceTitlePipe();
|
const pipe = new DeviceTitlePipe();
|
||||||
expect(pipe).toBeTruthy();
|
expect(pipe).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#deviceTitleForType',() => {
|
describe('#deviceTitleForType', () => {
|
||||||
const testCases = [
|
const testCases = [
|
||||||
{
|
{
|
||||||
'device': {
|
'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';
|
import {DeviceModel} from 'app/core/models/device-model';
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
@@ -36,7 +36,7 @@ export class DeviceTitlePipe implements PipeTransform {
|
|||||||
return titleParts.join(' - ')
|
return titleParts.join(' - ')
|
||||||
}
|
}
|
||||||
|
|
||||||
static deviceTitleWithFallback(device, titleType: string): string {
|
static deviceTitleWithFallback(device: DeviceModel, titleType: string): string {
|
||||||
console.log(`Displaying Device ${device.wwn} with: ${titleType}`)
|
console.log(`Displaying Device ${device.wwn} with: ${titleType}`)
|
||||||
const titleParts = []
|
const titleParts = []
|
||||||
if (device.host_id) titleParts.push(device.host_id)
|
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)
|
return DeviceTitlePipe.deviceTitleWithFallback(device, titleType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user