From b71d6660a6a67b6d51a067b63f01775a2cf712da Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Fri, 8 Jul 2022 18:19:07 -0700 Subject: [PATCH] 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. --- .github/workflows/ci.yaml | 47 +- Makefile | 10 + .../pkg/web/handler/get_devices_summary.go | 1 + webapp/frontend/.gitignore | 2 + webapp/frontend/karma.conf.js | 4 +- .../src/app/core/config/app.config.ts | 18 +- .../models/device-details-response-wrapper.ts | 14 + .../src/app/core/models/device-model.ts | 8 +- .../app/core/models/device-summary-model.ts | 16 + .../models/device-summary-response-wrapper.ts | 10 + .../device-summary-temp-response-wrapper.ts | 9 + .../measurements/smart-attribute-model.ts | 19 + .../core/models/measurements/smart-model.ts | 13 + .../measurements/smart-temperature-model.ts | 6 + .../thresholds/attribute-metadata-model.ts | 13 + .../src/app/data/mock/summary/temp_history.ts | 1200 +++++++++++++++++ ...ard-device-delete-dialog.component.spec.ts | 81 +- ...ashboard-device-delete-dialog.component.ts | 3 +- .../dashboard-device-delete-dialog.module.ts | 37 +- .../dashboard-device.component.spec.ts | 118 +- .../dashboard-device.component.ts | 37 +- .../dashboard-device.module.ts | 41 +- .../dashboard-settings.component.spec.ts | 25 - .../dashboard-settings.component.ts | 40 +- .../detail-settings/detail-settings.module.ts | 32 +- .../dashboard/dashboard.component.html | 2 +- .../modules/dashboard/dashboard.component.ts | 70 +- .../modules/dashboard/dashboard.resolvers.ts | 15 +- .../dashboard/dashboard.service.spec.ts | 44 + .../modules/dashboard/dashboard.service.ts | 42 +- .../modules/detail/detail.component.spec.ts | 38 - .../app/modules/detail/detail.component.ts | 240 ++-- .../app/modules/detail/detail.resolvers.ts | 15 +- .../app/modules/detail/detail.service.spec.ts | 28 + .../src/app/modules/detail/detail.service.ts | 27 +- .../src/app/shared/device-title.pipe.spec.ts | 15 +- .../src/app/shared/device-title.pipe.ts | 6 +- 37 files changed, 1897 insertions(+), 449 deletions(-) create mode 100644 webapp/frontend/src/app/core/models/device-details-response-wrapper.ts create mode 100644 webapp/frontend/src/app/core/models/device-summary-model.ts create mode 100644 webapp/frontend/src/app/core/models/device-summary-response-wrapper.ts create mode 100644 webapp/frontend/src/app/core/models/device-summary-temp-response-wrapper.ts create mode 100644 webapp/frontend/src/app/core/models/measurements/smart-attribute-model.ts create mode 100644 webapp/frontend/src/app/core/models/measurements/smart-model.ts create mode 100644 webapp/frontend/src/app/core/models/measurements/smart-temperature-model.ts create mode 100644 webapp/frontend/src/app/core/models/thresholds/attribute-metadata-model.ts create mode 100644 webapp/frontend/src/app/data/mock/summary/temp_history.ts delete mode 100644 webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.spec.ts create mode 100644 webapp/frontend/src/app/modules/dashboard/dashboard.service.spec.ts delete mode 100644 webapp/frontend/src/app/modules/detail/detail.component.spec.ts create mode 100644 webapp/frontend/src/app/modules/detail/detail.service.spec.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1310337..233fa26 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 }} diff --git a/Makefile b/Makefile index afb2d84..176d325 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/webapp/backend/pkg/web/handler/get_devices_summary.go b/webapp/backend/pkg/web/handler/get_devices_summary.go index 8eb392f..56e3eb5 100644 --- a/webapp/backend/pkg/web/handler/get_devices_summary.go +++ b/webapp/backend/pkg/web/handler/get_devices_summary.go @@ -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{}{ diff --git a/webapp/frontend/.gitignore b/webapp/frontend/.gitignore index 10fbf55..dd9d262 100644 --- a/webapp/frontend/.gitignore +++ b/webapp/frontend/.gitignore @@ -46,3 +46,5 @@ testem.log Thumbs.db /dist + +/coverage diff --git a/webapp/frontend/karma.conf.js b/webapp/frontend/karma.conf.js index 04347ed..4ccca41 100644 --- a/webapp/frontend/karma.conf.js +++ b/webapp/frontend/karma.conf.js @@ -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'], diff --git a/webapp/frontend/src/app/core/config/app.config.ts b/webapp/frontend/src/app/core/config/app.config.ts index e98a6c8..74143c5 100644 --- a/webapp/frontend/src/app/core/config/app.config.ts +++ b/webapp/frontend/src/app/core/config/app.config.ts @@ -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; } /** diff --git a/webapp/frontend/src/app/core/models/device-details-response-wrapper.ts b/webapp/frontend/src/app/core/models/device-details-response-wrapper.ts new file mode 100644 index 0000000..610af90 --- /dev/null +++ b/webapp/frontend/src/app/core/models/device-details-response-wrapper.ts @@ -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 }; +} diff --git a/webapp/frontend/src/app/core/models/device-model.ts b/webapp/frontend/src/app/core/models/device-model.ts index ae7e5b0..7613c3f 100644 --- a/webapp/frontend/src/app/core/models/device-model.ts +++ b/webapp/frontend/src/app/core/models/device-model.ts @@ -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; diff --git a/webapp/frontend/src/app/core/models/device-summary-model.ts b/webapp/frontend/src/app/core/models/device-summary-model.ts new file mode 100644 index 0000000..daf7ee4 --- /dev/null +++ b/webapp/frontend/src/app/core/models/device-summary-model.ts @@ -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 +} + diff --git a/webapp/frontend/src/app/core/models/device-summary-response-wrapper.ts b/webapp/frontend/src/app/core/models/device-summary-response-wrapper.ts new file mode 100644 index 0000000..cffac38 --- /dev/null +++ b/webapp/frontend/src/app/core/models/device-summary-response-wrapper.ts @@ -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 } + } +} diff --git a/webapp/frontend/src/app/core/models/device-summary-temp-response-wrapper.ts b/webapp/frontend/src/app/core/models/device-summary-temp-response-wrapper.ts new file mode 100644 index 0000000..c234fdd --- /dev/null +++ b/webapp/frontend/src/app/core/models/device-summary-temp-response-wrapper.ts @@ -0,0 +1,9 @@ +import {SmartTemperatureModel} from './measurements/smart-temperature-model'; + +export interface DeviceSummaryTempResponseWrapper { + success: boolean; + errors: any[]; + data: { + temp_history: { [key: string]: SmartTemperatureModel[]; } + } +} diff --git a/webapp/frontend/src/app/core/models/measurements/smart-attribute-model.ts b/webapp/frontend/src/app/core/models/measurements/smart-attribute-model.ts new file mode 100644 index 0000000..3a253b7 --- /dev/null +++ b/webapp/frontend/src/app/core/models/measurements/smart-attribute-model.ts @@ -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[] +} diff --git a/webapp/frontend/src/app/core/models/measurements/smart-model.ts b/webapp/frontend/src/app/core/models/measurements/smart-model.ts new file mode 100644 index 0000000..d44d2e1 --- /dev/null +++ b/webapp/frontend/src/app/core/models/measurements/smart-model.ts @@ -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 } +} diff --git a/webapp/frontend/src/app/core/models/measurements/smart-temperature-model.ts b/webapp/frontend/src/app/core/models/measurements/smart-temperature-model.ts new file mode 100644 index 0000000..3b05313 --- /dev/null +++ b/webapp/frontend/src/app/core/models/measurements/smart-temperature-model.ts @@ -0,0 +1,6 @@ +// maps to webapp/backend/pkg/models/measurements/smart_temperature.go +export interface SmartTemperatureModel { + date: string; + temp: number; +} + diff --git a/webapp/frontend/src/app/core/models/thresholds/attribute-metadata-model.ts b/webapp/frontend/src/app/core/models/thresholds/attribute-metadata-model.ts new file mode 100644 index 0000000..02a4e94 --- /dev/null +++ b/webapp/frontend/src/app/core/models/thresholds/attribute-metadata-model.ts @@ -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 +} diff --git a/webapp/frontend/src/app/data/mock/summary/temp_history.ts b/webapp/frontend/src/app/data/mock/summary/temp_history.ts new file mode 100644 index 0000000..80e6b45 --- /dev/null +++ b/webapp/frontend/src/app/data/mock/summary/temp_history.ts @@ -0,0 +1,1200 @@ +/* tslint:disable */ +export const temp_history = { + "data": { + "temp_history": { + "0x5000cca252c859cc": [{ + "date": "2022-07-01T22:00:00Z", + "temp": 36 + }, { + "date": "2022-07-01T23:00:00Z", + "temp": 36 + }, { + "date": "2022-07-02T00:00:00Z", + "temp": 36 + }, { + "date": "2022-07-02T01:00:00Z", + "temp": 36 + }, { + "date": "2022-07-02T22:00:00Z", + "temp": 36 + }, { + "date": "2022-07-02T23:00:00Z", + "temp": 36 + }, { + "date": "2022-07-03T00:00:00Z", + "temp": 36 + }, { + "date": "2022-07-03T01:00:00Z", + "temp": 36 + }, { + "date": "2022-07-03T22:00:00Z", + "temp": 42 + }, { + "date": "2022-07-03T23:00:00Z", + "temp": 42 + }, { + "date": "2022-07-04T00:00:00Z", + "temp": 42 + }, { + "date": "2022-07-04T01:00:00Z", + "temp": 42 + }, { + "date": "2022-07-04T22:00:00Z", + "temp": 37 + }, { + "date": "2022-07-04T23:00:00Z", + "temp": 36 + }, { + "date": "2022-07-05T00:00:00Z", + "temp": 36 + }, { + "date": "2022-07-05T01:00:00Z", + "temp": 37 + }, { + "date": "2022-07-05T22:00:00Z", + "temp": 37 + }, { + "date": "2022-07-05T23:00:00Z", + "temp": 37 + }, { + "date": "2022-07-06T00:00:00Z", + "temp": 37 + }, { + "date": "2022-07-06T01:00:00Z", + "temp": 37 + }, { + "date": "2022-07-06T04:00:00Z", + "temp": 37 + }, { + "date": "2022-07-06T05:00:00Z", + "temp": 37 + }, { + "date": "2022-07-06T06:00:00Z", + "temp": 37 + }, { + "date": "2022-07-06T22:00:00Z", + "temp": 38 + }, { + "date": "2022-07-06T23:00:00Z", + "temp": 38 + }, { + "date": "2022-07-07T00:00:00Z", + "temp": 38 + }, { + "date": "2022-07-07T01:00:00Z", + "temp": 38 + }, { + "date": "2022-07-07T03:00:00Z", + "temp": 37 + }, { + "date": "2022-07-07T04:00:00Z", + "temp": 37 + }, { + "date": "2022-07-07T05:00:00Z", + "temp": 37 + }, { + "date": "2022-07-07T06:00:00Z", + "temp": 37 + }, { + "date": "2022-07-07T07:00:00Z", + "temp": 38 + }, { + "date": "2022-07-07T22:00:00Z", + "temp": 37 + }, { + "date": "2022-07-07T23:00:00Z", + "temp": 37 + }, { + "date": "2022-07-08T00:00:00Z", + "temp": 37 + }, { + "date": "2022-07-08T01:00:00Z", + "temp": 37 + }, { + "date": "2022-07-08T13:00:00Z", + "temp": 35 + }, { + "date": "2022-07-08T14:00:00Z", + "temp": 35 + }, { + "date": "2022-07-08T15:00:00Z", + "temp": 35 + }], + "0x5000cca264eb01d7": [{ + "date": "2022-07-01T22:00:00Z", + "temp": 42 + }, { + "date": "2022-07-01T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-02T00:00:00Z", + "temp": 39 + }, { + "date": "2022-07-02T01:00:00Z", + "temp": 39 + }, { + "date": "2022-07-02T22:00:00Z", + "temp": 41 + }, { + "date": "2022-07-02T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-03T00:00:00Z", + "temp": 39 + }, { + "date": "2022-07-03T01:00:00Z", + "temp": 40 + }, { + "date": "2022-07-03T22:00:00Z", + "temp": 44 + }, { + "date": "2022-07-03T23:00:00Z", + "temp": 42 + }, { + "date": "2022-07-04T00:00:00Z", + "temp": 41 + }, { + "date": "2022-07-04T01:00:00Z", + "temp": 41 + }, { + "date": "2022-07-04T22:00:00Z", + "temp": 41 + }, { + "date": "2022-07-04T23:00:00Z", + "temp": 42 + }, { + "date": "2022-07-05T00:00:00Z", + "temp": 40 + }, { + "date": "2022-07-05T01:00:00Z", + "temp": 41 + }, { + "date": "2022-07-05T22:00:00Z", + "temp": 40 + }, { + "date": "2022-07-05T23:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T00:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T01:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T04:00:00Z", + "temp": 42 + }, { + "date": "2022-07-06T05:00:00Z", + "temp": 45 + }, { + "date": "2022-07-06T06:00:00Z", + "temp": 44 + }, { + "date": "2022-07-06T22:00:00Z", + "temp": 41 + }, { + "date": "2022-07-06T23:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T00:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T01:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T03:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T04:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T05:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T06:00:00Z", + "temp": 42 + }, { + "date": "2022-07-07T07:00:00Z", + "temp": 42 + }, { + "date": "2022-07-07T22:00:00Z", + "temp": 40 + }, { + "date": "2022-07-07T23:00:00Z", + "temp": 40 + }, { + "date": "2022-07-08T00:00:00Z", + "temp": 40 + }, { + "date": "2022-07-08T01:00:00Z", + "temp": 40 + }, { + "date": "2022-07-08T13:00:00Z", + "temp": 39 + }, { + "date": "2022-07-08T14:00:00Z", + "temp": 39 + }, { + "date": "2022-07-08T15:00:00Z", + "temp": 39 + }], + "0x5000cca264ebc248": [{ + "date": "2022-07-01T22:00:00Z", + "temp": 36 + }, { + "date": "2022-07-01T23:00:00Z", + "temp": 34 + }, { + "date": "2022-07-02T00:00:00Z", + "temp": 33 + }, { + "date": "2022-07-02T01:00:00Z", + "temp": 33 + }, { + "date": "2022-07-02T22:00:00Z", + "temp": 35 + }, { + "date": "2022-07-02T23:00:00Z", + "temp": 34 + }, { + "date": "2022-07-03T00:00:00Z", + "temp": 33 + }, { + "date": "2022-07-03T01:00:00Z", + "temp": 33 + }, { + "date": "2022-07-03T22:00:00Z", + "temp": 37 + }, { + "date": "2022-07-03T23:00:00Z", + "temp": 37 + }, { + "date": "2022-07-04T00:00:00Z", + "temp": 33 + }, { + "date": "2022-07-04T01:00:00Z", + "temp": 33 + }, { + "date": "2022-07-04T22:00:00Z", + "temp": 35 + }, { + "date": "2022-07-04T23:00:00Z", + "temp": 35 + }, { + "date": "2022-07-05T00:00:00Z", + "temp": 35 + }, { + "date": "2022-07-05T01:00:00Z", + "temp": 35 + }, { + "date": "2022-07-05T22:00:00Z", + "temp": 36 + }, { + "date": "2022-07-05T23:00:00Z", + "temp": 35 + }, { + "date": "2022-07-06T00:00:00Z", + "temp": 35 + }, { + "date": "2022-07-06T01:00:00Z", + "temp": 36 + }, { + "date": "2022-07-06T04:00:00Z", + "temp": 38 + }, { + "date": "2022-07-06T05:00:00Z", + "temp": 39 + }, { + "date": "2022-07-06T06:00:00Z", + "temp": 38 + }, { + "date": "2022-07-06T22:00:00Z", + "temp": 35 + }, { + "date": "2022-07-06T23:00:00Z", + "temp": 35 + }, { + "date": "2022-07-07T00:00:00Z", + "temp": 35 + }, { + "date": "2022-07-07T01:00:00Z", + "temp": 35 + }, { + "date": "2022-07-07T03:00:00Z", + "temp": 38 + }, { + "date": "2022-07-07T04:00:00Z", + "temp": 36 + }, { + "date": "2022-07-07T05:00:00Z", + "temp": 35 + }, { + "date": "2022-07-07T06:00:00Z", + "temp": 37 + }, { + "date": "2022-07-07T07:00:00Z", + "temp": 36 + }, { + "date": "2022-07-07T22:00:00Z", + "temp": 34 + }, { + "date": "2022-07-07T23:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T00:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T01:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T13:00:00Z", + "temp": 33 + }, { + "date": "2022-07-08T14:00:00Z", + "temp": 33 + }, { + "date": "2022-07-08T15:00:00Z", + "temp": 33 + }], + "0x5000cca264ec3183": [{ + "date": "2022-07-01T22:00:00Z", + "temp": 39 + }, { + "date": "2022-07-01T23:00:00Z", + "temp": 38 + }, { + "date": "2022-07-02T00:00:00Z", + "temp": 37 + }, { + "date": "2022-07-02T01:00:00Z", + "temp": 37 + }, { + "date": "2022-07-02T22:00:00Z", + "temp": 39 + }, { + "date": "2022-07-02T23:00:00Z", + "temp": 37 + }, { + "date": "2022-07-03T00:00:00Z", + "temp": 39 + }, { + "date": "2022-07-03T01:00:00Z", + "temp": 40 + }, { + "date": "2022-07-03T22:00:00Z", + "temp": 40 + }, { + "date": "2022-07-03T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-04T00:00:00Z", + "temp": 38 + }, { + "date": "2022-07-04T01:00:00Z", + "temp": 38 + }, { + "date": "2022-07-04T22:00:00Z", + "temp": 38 + }, { + "date": "2022-07-04T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-05T00:00:00Z", + "temp": 38 + }, { + "date": "2022-07-05T01:00:00Z", + "temp": 38 + }, { + "date": "2022-07-05T22:00:00Z", + "temp": 38 + }, { + "date": "2022-07-05T23:00:00Z", + "temp": 38 + }, { + "date": "2022-07-06T00:00:00Z", + "temp": 38 + }, { + "date": "2022-07-06T01:00:00Z", + "temp": 38 + }, { + "date": "2022-07-06T04:00:00Z", + "temp": 41 + }, { + "date": "2022-07-06T05:00:00Z", + "temp": 42 + }, { + "date": "2022-07-06T06:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T22:00:00Z", + "temp": 39 + }, { + "date": "2022-07-06T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-07T00:00:00Z", + "temp": 39 + }, { + "date": "2022-07-07T01:00:00Z", + "temp": 39 + }, { + "date": "2022-07-07T03:00:00Z", + "temp": 40 + }, { + "date": "2022-07-07T04:00:00Z", + "temp": 40 + }, { + "date": "2022-07-07T05:00:00Z", + "temp": 39 + }, { + "date": "2022-07-07T06:00:00Z", + "temp": 40 + }, { + "date": "2022-07-07T07:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T22:00:00Z", + "temp": 38 + }, { + "date": "2022-07-07T23:00:00Z", + "temp": 38 + }, { + "date": "2022-07-08T00:00:00Z", + "temp": 38 + }, { + "date": "2022-07-08T01:00:00Z", + "temp": 38 + }, { + "date": "2022-07-08T13:00:00Z", + "temp": 37 + }, { + "date": "2022-07-08T14:00:00Z", + "temp": 37 + }, { + "date": "2022-07-08T15:00:00Z", + "temp": 37 + }], + "0x5000cca28ed7fcd8": [{ + "date": "2022-07-01T22:00:00Z", + "temp": 36 + }, { + "date": "2022-07-01T23:00:00Z", + "temp": 34 + }, { + "date": "2022-07-02T00:00:00Z", + "temp": 34 + }, { + "date": "2022-07-02T01:00:00Z", + "temp": 34 + }, { + "date": "2022-07-02T22:00:00Z", + "temp": 35 + }, { + "date": "2022-07-02T23:00:00Z", + "temp": 34 + }, { + "date": "2022-07-03T00:00:00Z", + "temp": 33 + }, { + "date": "2022-07-03T01:00:00Z", + "temp": 33 + }, { + "date": "2022-07-03T22:00:00Z", + "temp": 36 + }, { + "date": "2022-07-03T23:00:00Z", + "temp": 36 + }, { + "date": "2022-07-04T00:00:00Z", + "temp": 33 + }, { + "date": "2022-07-04T01:00:00Z", + "temp": 33 + }, { + "date": "2022-07-04T22:00:00Z", + "temp": 39 + }, { + "date": "2022-07-04T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-05T00:00:00Z", + "temp": 39 + }, { + "date": "2022-07-05T01:00:00Z", + "temp": 39 + }, { + "date": "2022-07-05T22:00:00Z", + "temp": 40 + }, { + "date": "2022-07-05T23:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T00:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T01:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T04:00:00Z", + "temp": 38 + }, { + "date": "2022-07-06T05:00:00Z", + "temp": 38 + }, { + "date": "2022-07-06T06:00:00Z", + "temp": 36 + }, { + "date": "2022-07-06T22:00:00Z", + "temp": 36 + }, { + "date": "2022-07-06T23:00:00Z", + "temp": 35 + }, { + "date": "2022-07-07T00:00:00Z", + "temp": 36 + }, { + "date": "2022-07-07T01:00:00Z", + "temp": 36 + }, { + "date": "2022-07-07T03:00:00Z", + "temp": 38 + }, { + "date": "2022-07-07T04:00:00Z", + "temp": 37 + }, { + "date": "2022-07-07T05:00:00Z", + "temp": 36 + }, { + "date": "2022-07-07T06:00:00Z", + "temp": 39 + }, { + "date": "2022-07-07T07:00:00Z", + "temp": 37 + }, { + "date": "2022-07-07T22:00:00Z", + "temp": 34 + }, { + "date": "2022-07-07T23:00:00Z", + "temp": 35 + }, { + "date": "2022-07-08T00:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T01:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T13:00:00Z", + "temp": 33 + }, { + "date": "2022-07-08T14:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T15:00:00Z", + "temp": 34 + }], + "0x5000cca28fc25581": [{ + "date": "2022-07-01T22:00:00Z", + "temp": 39 + }, { + "date": "2022-07-01T23:00:00Z", + "temp": 38 + }, { + "date": "2022-07-02T00:00:00Z", + "temp": 38 + }, { + "date": "2022-07-02T01:00:00Z", + "temp": 38 + }, { + "date": "2022-07-02T22:00:00Z", + "temp": 39 + }, { + "date": "2022-07-02T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-03T00:00:00Z", + "temp": 39 + }, { + "date": "2022-07-03T01:00:00Z", + "temp": 39 + }, { + "date": "2022-07-03T22:00:00Z", + "temp": 46 + }, { + "date": "2022-07-03T23:00:00Z", + "temp": 46 + }, { + "date": "2022-07-04T00:00:00Z", + "temp": 46 + }, { + "date": "2022-07-04T01:00:00Z", + "temp": 46 + }, { + "date": "2022-07-04T22:00:00Z", + "temp": 40 + }, { + "date": "2022-07-04T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-05T00:00:00Z", + "temp": 39 + }, { + "date": "2022-07-05T01:00:00Z", + "temp": 41 + }, { + "date": "2022-07-05T22:00:00Z", + "temp": 40 + }, { + "date": "2022-07-05T23:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T00:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T01:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T04:00:00Z", + "temp": 41 + }, { + "date": "2022-07-06T05:00:00Z", + "temp": 41 + }, { + "date": "2022-07-06T06:00:00Z", + "temp": 41 + }, { + "date": "2022-07-06T22:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T23:00:00Z", + "temp": 40 + }, { + "date": "2022-07-07T00:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T01:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T03:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T04:00:00Z", + "temp": 40 + }, { + "date": "2022-07-07T05:00:00Z", + "temp": 40 + }, { + "date": "2022-07-07T06:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T07:00:00Z", + "temp": 40 + }, { + "date": "2022-07-07T22:00:00Z", + "temp": 39 + }, { + "date": "2022-07-07T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-08T00:00:00Z", + "temp": 39 + }, { + "date": "2022-07-08T01:00:00Z", + "temp": 39 + }, { + "date": "2022-07-08T13:00:00Z", + "temp": 38 + }, { + "date": "2022-07-08T14:00:00Z", + "temp": 38 + }, { + "date": "2022-07-08T15:00:00Z", + "temp": 38 + }], + "0x5002538e40a22954": [{ + "date": "2022-07-01T19:00:00Z", + "temp": 30 + }, { + "date": "2022-07-01T20:00:00Z", + "temp": 31 + }, { + "date": "2022-07-01T21:00:00Z", + "temp": 31 + }, { + "date": "2022-07-01T22:00:00Z", + "temp": 31 + }, { + "date": "2022-07-01T23:00:00Z", + "temp": 30 + }, { + "date": "2022-07-02T00:00:00Z", + "temp": 30 + }, { + "date": "2022-07-02T01:00:00Z", + "temp": 31 + }, { + "date": "2022-07-02T03:00:00Z", + "temp": 31 + }, { + "date": "2022-07-02T04:00:00Z", + "temp": 30 + }, { + "date": "2022-07-02T05:00:00Z", + "temp": 30 + }, { + "date": "2022-07-02T06:00:00Z", + "temp": 30 + }, { + "date": "2022-07-02T07:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T08:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T09:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T10:00:00Z", + "temp": 30 + }, { + "date": "2022-07-02T11:00:00Z", + "temp": 30 + }, { + "date": "2022-07-02T12:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T13:00:00Z", + "temp": 28 + }, { + "date": "2022-07-02T14:00:00Z", + "temp": 28 + }, { + "date": "2022-07-02T15:00:00Z", + "temp": 28 + }, { + "date": "2022-07-02T16:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T17:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T18:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T19:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T20:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T21:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T22:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T23:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T00:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T01:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T03:00:00Z", + "temp": 32 + }, { + "date": "2022-07-03T04:00:00Z", + "temp": 31 + }, { + "date": "2022-07-03T05:00:00Z", + "temp": 30 + }, { + "date": "2022-07-03T06:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T07:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T08:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T09:00:00Z", + "temp": 30 + }, { + "date": "2022-07-03T10:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T11:00:00Z", + "temp": 31 + }, { + "date": "2022-07-03T12:00:00Z", + "temp": 30 + }, { + "date": "2022-07-03T13:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T14:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T15:00:00Z", + "temp": 30 + }, { + "date": "2022-07-03T16:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T17:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T18:00:00Z", + "temp": 30 + }, { + "date": "2022-07-03T19:00:00Z", + "temp": 30 + }, { + "date": "2022-07-03T20:00:00Z", + "temp": 30 + }, { + "date": "2022-07-03T21:00:00Z", + "temp": 31 + }, { + "date": "2022-07-03T22:00:00Z", + "temp": 32 + }, { + "date": "2022-07-03T23:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T00:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T01:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T03:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T04:00:00Z", + "temp": 32 + }, { + "date": "2022-07-04T05:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T06:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T07:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T08:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T09:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T10:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T11:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T12:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T13:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T14:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T15:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T16:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T17:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T18:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T19:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T20:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T21:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T22:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T23:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T00:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T01:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T03:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T04:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T05:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T06:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T07:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T08:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T09:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T10:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T11:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T12:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T13:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T14:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T15:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T16:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T17:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T18:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T19:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T20:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T21:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T22:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T23:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T00:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T01:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T02:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T03:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T04:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T05:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T06:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T07:00:00Z", + "temp": 37 + }, { + "date": "2022-07-06T08:00:00Z", + "temp": 36 + }, { + "date": "2022-07-06T09:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T10:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T11:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T12:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T13:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T14:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T15:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T16:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T17:00:00Z", + "temp": 33 + }, { + "date": "2022-07-06T18:00:00Z", + "temp": 33 + }, { + "date": "2022-07-06T19:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T20:00:00Z", + "temp": 34 + }, { + "date": "2022-07-06T21:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T22:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T23:00:00Z", + "temp": 31 + }, { + "date": "2022-07-07T00:00:00Z", + "temp": 31 + }, { + "date": "2022-07-07T01:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T02:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T03:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T04:00:00Z", + "temp": 31 + }, { + "date": "2022-07-07T05:00:00Z", + "temp": 31 + }, { + "date": "2022-07-07T06:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T07:00:00Z", + "temp": 31 + }, { + "date": "2022-07-07T08:00:00Z", + "temp": 31 + }, { + "date": "2022-07-07T09:00:00Z", + "temp": 31 + }, { + "date": "2022-07-07T10:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T11:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T12:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T13:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T14:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T15:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T16:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T17:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T18:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T19:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T20:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T21:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T22:00:00Z", + "temp": 31 + }, { + "date": "2022-07-07T23:00:00Z", + "temp": 31 + }, { + "date": "2022-07-08T00:00:00Z", + "temp": 31 + }, { + "date": "2022-07-08T01:00:00Z", + "temp": 31 + }, { + "date": "2022-07-08T02:00:00Z", + "temp": 30 + }, { + "date": "2022-07-08T03:00:00Z", + "temp": 31 + }, { + "date": "2022-07-08T04:00:00Z", + "temp": 31 + }, { + "date": "2022-07-08T05:00:00Z", + "temp": 32 + }, { + "date": "2022-07-08T06:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T07:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T08:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T09:00:00Z", + "temp": 31 + }, { + "date": "2022-07-08T10:00:00Z", + "temp": 30 + }, { + "date": "2022-07-08T11:00:00Z", + "temp": 30 + }, { + "date": "2022-07-08T12:00:00Z", + "temp": 30 + }, { + "date": "2022-07-08T13:00:00Z", + "temp": 31 + }, { + "date": "2022-07-08T14:00:00Z", + "temp": 31 + }, { + "date": "2022-07-08T15:00:00Z", + "temp": 31 + }] + } + }, + "success": true +} diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.spec.ts b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.spec.ts index db01c53..26248f1 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.spec.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.spec.ts @@ -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; - - 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; + + 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); + }); }); diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.ts b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.ts index d995887..5fdd8a0 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.ts @@ -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', diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module.ts b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module.ts index 2605777..5d3799f 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module.ts @@ -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 ], diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.spec.ts b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.spec.ts index dba412c..7b334bb 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.spec.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.spec.ts @@ -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; + let component: DashboardDeviceComponent; + let fixture: ComponentFixture; - 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(); - }); + }); diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts index d2c9859..4fb7d7a 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts @@ -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(); @@ -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) } }); diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.module.ts b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.module.ts index e338330..924c145 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.module.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.module.ts @@ -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 { } diff --git a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.spec.ts b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.spec.ts deleted file mode 100644 index 95a052f..0000000 --- a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.spec.ts +++ /dev/null @@ -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; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ DashboardSettingsComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(DashboardSettingsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts index 893aadf..70a0978 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts @@ -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; diff --git a/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.module.ts b/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.module.ts index b8c05df..a85e0ca 100644 --- a/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.module.ts +++ b/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.module.ts @@ -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 { } diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html index cbb19a4..d370ab5 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html @@ -1,5 +1,5 @@ -
+
diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts index eb5a2cc..7352e98 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts @@ -1,17 +1,24 @@ -import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; -import { MatSort } from '@angular/material/sort'; -import { MatTableDataSource } from '@angular/material/table'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + OnDestroy, + OnInit, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import {Subject} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; import {ApexOptions, ChartComponent} from 'ng-apexcharts'; -import { DashboardService } from 'app/modules/dashboard/dashboard.service'; +import {DashboardService} from 'app/modules/dashboard/dashboard.service'; import {MatDialog} from '@angular/material/dialog'; -import { DashboardSettingsComponent } from 'app/layout/common/dashboard-settings/dashboard-settings.component'; +import {DashboardSettingsComponent} from 'app/layout/common/dashboard-settings/dashboard-settings.component'; import {AppConfig} from 'app/core/config/app.config'; import {TreoConfigService} from '@treo/services/config'; import {Router} from '@angular/router'; import {TemperaturePipe} from 'app/shared/temperature.pipe'; import {DeviceTitlePipe} from 'app/shared/device-title.pipe'; +import {DeviceSummaryModel} from 'app/core/models/device-summary-model'; @Component({ selector : 'example', @@ -22,7 +29,7 @@ import {DeviceTitlePipe} from 'app/shared/device-title.pipe'; }) export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy { - data: any; + summaryData: { [key: string]: DeviceSummaryModel }; hostGroups: { [hostId: string]: string[] } = {} temperatureOptions: ApexOptions; tempDurationKey = 'forever' @@ -35,10 +42,13 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy /** * Constructor * - * @param {SmartService} _smartService + * @param {DashboardService} _dashboardService + * @param {TreoConfigService} _configService + * @param {MatDialog} dialog + * @param {Router} router */ constructor( - private _smartService: DashboardService, + private _dashboardService: DashboardService, private _configService: TreoConfigService, public dialog: MatDialog, private router: Router, @@ -81,16 +91,16 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy }); // Get the data - this._smartService.data$ + this._dashboardService.data$ .pipe(takeUntil(this._unsubscribeAll)) .subscribe((data) => { // Store the data - this.data = data; + this.summaryData = data; // generate group data. - for(const wwn in this.data.data.summary){ - const hostid = this.data.data.summary[wwn].device.host_id + for (const wwn in this.summaryData) { + const hostid = this.summaryData[wwn].device.host_id const hostDeviceList = this.hostGroups[hostid] || [] hostDeviceList.push(wwn) this.hostGroups[hostid] = hostDeviceList @@ -132,11 +142,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy private _deviceDataTemperatureSeries(): any[] { const deviceTemperatureSeries = [] - console.log('DEVICE DATA SUMMARY', this.data) + console.log('DEVICE DATA SUMMARY', this.summaryData) - for(const wwn in this.data.data.summary){ - const deviceSummary = this.data.data.summary[wwn] - if (!deviceSummary.temp_history){ + for (const wwn in this.summaryData) { + const deviceSummary = this.summaryData[wwn] + if (!deviceSummary.temp_history) { continue } @@ -206,7 +216,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy } } }, - xaxis : { + xaxis: { type: 'datetime' } }; @@ -216,11 +226,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy // @ Public methods // ----------------------------------------------------------------------------------------------------- - deviceSummariesForHostGroup(hostGroupWWNs: string[]): any[] { - const deviceSummaries = [] - for(const wwn of hostGroupWWNs){ - if(this.data.data.summary[wwn]){ - deviceSummaries.push(this.data.data.summary[wwn]) + deviceSummariesForHostGroup(hostGroupWWNs: string[]): DeviceSummaryModel[] { + const deviceSummaries: DeviceSummaryModel[] = [] + for (const wwn of hostGroupWWNs) { + if (this.summaryData[wwn]) { + deviceSummaries.push(this.summaryData[wwn]) } } return deviceSummaries @@ -235,7 +245,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy } onDeviceDeleted(wwn: string): void { - delete this.data.data.summary[wwn] // remove the device from the summary list. + delete this.summaryData[wwn] // remove the device from the summary list. } /* @@ -246,16 +256,16 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy DURATION_KEY_FOREVER = "forever" */ - changeSummaryTempDuration(durationKey: string){ + changeSummaryTempDuration(durationKey: string): void { this.tempDurationKey = durationKey - this._smartService.getSummaryTempData(durationKey) - .subscribe((data) => { + this._dashboardService.getSummaryTempData(durationKey) + .subscribe((tempHistoryData) => { // given a list of device temp history, override the data in the "summary" object. - for(const wwn in this.data.data.summary) { + for (const wwn in this.summaryData) { // console.log(`Updating ${wwn}, length: ${this.data.data.summary[wwn].temp_history.length}`) - this.data.data.summary[wwn].temp_history = data.data.temp_history[wwn] || [] + this.summaryData[wwn].temp_history = tempHistoryData[wwn] || [] } // Prepare the chart series data diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts index eb29b48..a235061 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts @@ -1,13 +1,13 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; -import { Observable } from 'rxjs'; -import { DashboardService } from 'app/modules/dashboard/dashboard.service'; +import {Injectable} from '@angular/core'; +import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router'; +import {Observable} from 'rxjs'; +import {DashboardService} from 'app/modules/dashboard/dashboard.service'; +import {DeviceSummaryModel} from 'app/core/models/device-summary-model'; @Injectable({ providedIn: 'root' }) -export class DashboardResolver implements Resolve -{ +export class DashboardResolver implements Resolve { /** * Constructor * @@ -29,8 +29,7 @@ export class DashboardResolver implements Resolve * @param route * @param state */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable - { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<{ [p: string]: DeviceSummaryModel }> { return this._dashboardService.getSummaryData(); } } diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.service.spec.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.service.spec.ts new file mode 100644 index 0000000..75a7679 --- /dev/null +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.service.spec.ts @@ -0,0 +1,44 @@ +import {HttpClient} from '@angular/common/http'; +import {DashboardService} from './dashboard.service'; +import {of} from 'rxjs'; +import {summary} from 'app/data/mock/summary/data' +import {temp_history} from 'app/data/mock/summary/temp_history' +import {DeviceSummaryModel} from 'app/core/models/device-summary-model'; +import {SmartTemperatureModel} from 'app/core/models/measurements/smart-temperature-model'; + +describe('DashboardService', () => { + let service: DashboardService; + let httpClientSpy: jasmine.SpyObj; + + beforeEach(() => { + httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']); + service = new DashboardService(httpClientSpy); + }); + + it('should unwrap and return getSummaryData() (HttpClient called once)', (done: DoneFn) => { + httpClientSpy.get.and.returnValue(of(summary)); + + service.getSummaryData().subscribe(value => { + expect(value).toBe(summary.data.summary as { [key: string]: DeviceSummaryModel }); + done(); + }); + expect(httpClientSpy.get.calls.count()) + .withContext('one call') + .toBe(1); + }); + + it('should unwrap and return getSummaryTempData() (HttpClient called once)', (done: DoneFn) => { + // const expectedHeroes: any[] = + // [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]; + + httpClientSpy.get.and.returnValue(of(temp_history)); + + service.getSummaryTempData('weekly').subscribe(value => { + expect(value).toBe(temp_history.data.temp_history as { [key: string]: SmartTemperatureModel[] }); + done(); + }); + expect(httpClientSpy.get.calls.count()) + .withContext('one call') + .toBe(1); + }); +}); diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts index 185da9d..35764d0 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts @@ -1,16 +1,19 @@ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { getBasePath } from 'app/app.routing'; +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {BehaviorSubject, Observable} from 'rxjs'; +import {map, tap} from 'rxjs/operators'; +import {getBasePath} from 'app/app.routing'; +import {DeviceSummaryResponseWrapper} from 'app/core/models/device-summary-response-wrapper'; +import {DeviceSummaryModel} from 'app/core/models/device-summary-model'; +import {SmartTemperatureModel} from 'app/core/models/measurements/smart-temperature-model'; +import {DeviceSummaryTempResponseWrapper} from 'app/core/models/device-summary-temp-response-wrapper'; @Injectable({ providedIn: 'root' }) -export class DashboardService -{ +export class DashboardService { // Observables - private _data: BehaviorSubject; + private _data: BehaviorSubject<{ [p: string]: DeviceSummaryModel }>; /** * Constructor @@ -32,8 +35,7 @@ export class DashboardService /** * Getter for data */ - get data$(): Observable - { + get data$(): Observable<{ [p: string]: DeviceSummaryModel }> { return this._data.asObservable(); } @@ -44,22 +46,28 @@ export class DashboardService /** * Get data */ - getSummaryData(): Observable - { + getSummaryData(): Observable<{ [key: string]: DeviceSummaryModel }> { return this._httpClient.get(getBasePath() + '/api/summary').pipe( - tap((response: any) => { + map((response: DeviceSummaryResponseWrapper) => { + // console.log("FILTERING=----", response.data.summary) + return response.data.summary + }), + tap((response: { [key: string]: DeviceSummaryModel }) => { this._data.next(response); }) ); } - getSummaryTempData(durationKey: string): Observable - { + getSummaryTempData(durationKey: string): Observable<{ [key: string]: SmartTemperatureModel[] }> { const params = {} - if(durationKey){ + if (durationKey) { params['duration_key'] = durationKey } - return this._httpClient.get(getBasePath() + '/api/summary/temp', {params: params}); + return this._httpClient.get(getBasePath() + '/api/summary/temp', {params: params}).pipe( + map((response: DeviceSummaryTempResponseWrapper) => { + return response.data.temp_history + }) + ); } } diff --git a/webapp/frontend/src/app/modules/detail/detail.component.spec.ts b/webapp/frontend/src/app/modules/detail/detail.component.spec.ts deleted file mode 100644 index 6f956ee..0000000 --- a/webapp/frontend/src/app/modules/detail/detail.component.spec.ts +++ /dev/null @@ -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; - - 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(); - }); -}); diff --git a/webapp/frontend/src/app/modules/detail/detail.component.ts b/webapp/frontend/src/app/modules/detail/detail.component.ts index f37910a..0e29652 100644 --- a/webapp/frontend/src/app/modules/detail/detail.component.ts +++ b/webapp/frontend/src/app/modules/detail/detail.component.ts @@ -1,18 +1,21 @@ -import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import humanizeDuration from 'humanize-duration'; +import {AfterViewInit, Component, Inject, LOCALE_ID, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {ApexOptions} from 'ng-apexcharts'; -import {MatTableDataSource} from '@angular/material/table'; -import {MatSort} from '@angular/material/sort'; -import {Subject} from 'rxjs'; +import {AppConfig} from 'app/core/config/app.config'; import {DetailService} from './detail.service'; -import {takeUntil} from 'rxjs/operators'; import {DetailSettingsComponent} from 'app/layout/common/detail-settings/detail-settings.component'; import {MatDialog} from '@angular/material/dialog'; -import humanizeDuration from 'humanize-duration'; +import {MatSort} from '@angular/material/sort'; +import {MatTableDataSource} from '@angular/material/table'; +import {Subject} from 'rxjs'; import {TreoConfigService} from '@treo/services/config'; -import {AppConfig} from 'app/core/config/app.config'; import {animate, state, style, transition, trigger} from '@angular/animations'; import {formatDate} from '@angular/common'; -import { LOCALE_ID, Inject } from '@angular/core'; +import {takeUntil} from 'rxjs/operators'; +import {DeviceModel} from 'app/core/models/device-model'; +import {SmartModel} from 'app/core/models/measurements/smart-model'; +import {SmartAttributeModel} from 'app/core/models/measurements/smart-attribute-model'; +import {AttributeMetadataModel} from 'app/core/models/thresholds/attribute-metadata-model'; // from Constants.go - these must match const AttributeStatusPassed = 0 @@ -22,9 +25,9 @@ const AttributeStatusFailedScrutiny = 4 @Component({ - selector: 'detail', - templateUrl: './detail.component.html', - styleUrls: ['./detail.component.scss'], + selector: 'detail', + templateUrl: './detail.component.html', + styleUrls: ['./detail.component.scss'], animations: [ trigger('detailExpand', [ state('collapsed', style({height: '0px', minHeight: '0'})), @@ -40,22 +43,23 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { * Constructor * * @param {DetailService} _detailService + * @param {MatDialog} dialog + * @param {TreoConfigService} _configService + * @param {string} locale */ constructor( private _detailService: DetailService, public dialog: MatDialog, private _configService: TreoConfigService, @Inject(LOCALE_ID) public locale: string - - ) - { + ) { // Set the private defaults this._unsubscribeAll = new Subject(); // Set the defaults this.smartAttributeDataSource = new MatTableDataSource(); // this.recentTransactionsTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh']; - this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh','ideal', 'failure', 'history']; + this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh', 'ideal', 'failure', 'history']; this.systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; @@ -65,14 +69,15 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { onlyCritical = true; // data: any; - expandedAttribute: any | null; + expandedAttribute: SmartAttributeModel | null; - metadata: any; - device: any; - smart_results: any[]; + metadata: { [p: string]: AttributeMetadataModel } | { [p: number]: AttributeMetadataModel }; + device: DeviceModel; + // tslint:disable-next-line:variable-name + smart_results: SmartModel[]; commonSparklineOptions: Partial; - smartAttributeDataSource: MatTableDataSource; + smartAttributeDataSource: MatTableDataSource; smartAttributeTableColumns: string[]; @ViewChild('smartAttributeTable', {read: MatSort}) @@ -91,8 +96,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { /** * On init */ - ngOnInit(): void - { + ngOnInit(): void { // Subscribe to config changes this._configService.config$ .pipe(takeUntil(this._unsubscribeAll)) @@ -104,13 +108,13 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { // Get the data this._detailService.data$ .pipe(takeUntil(this._unsubscribeAll)) - .subscribe((data) => { + .subscribe((respWrapper) => { // Store the data // this.data = data; - this.device = data.data.device; - this.smart_results = data.data.smart_results - this.metadata = data.metadata; + this.device = respWrapper.data.device; + this.smart_results = respWrapper.data.smart_results + this.metadata = respWrapper.metadata; // Store the table data @@ -124,8 +128,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { /** * After view init */ - ngAfterViewInit(): void - { + ngAfterViewInit(): void { // Make the data source sortable this.smartAttributeDataSource.sort = this.smartAttributeTableMatSort; } @@ -133,8 +136,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { /** * On destroy */ - ngOnDestroy(): void - { + ngOnDestroy(): void { // Unsubscribe from all subscriptions this._unsubscribeAll.next(); this._unsubscribeAll.complete(); @@ -147,22 +149,23 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { getAttributeStatusName(attributeStatus: number): string { // tslint:disable:no-bitwise - if(attributeStatus === AttributeStatusPassed){ + if (attributeStatus === AttributeStatusPassed) { return 'passed' - } else if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0 || (attributeStatus & AttributeStatusFailedSmart) !== 0 ){ + } else if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0 || (attributeStatus & AttributeStatusFailedSmart) !== 0) { return 'failed' - } else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0){ + } else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0) { return 'warn' } return '' // tslint:enable:no-bitwise } + getAttributeScrutinyStatusName(attributeStatus: number): string { // tslint:disable:no-bitwise - if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0){ + if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0) { return 'failed' - } else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0){ + } else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0) { return 'warn' } else { return 'passed' @@ -172,7 +175,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { getAttributeSmartStatusName(attributeStatus: number): string { // tslint:disable:no-bitwise - if ((attributeStatus & AttributeStatusFailedSmart) !== 0){ + if ((attributeStatus & AttributeStatusFailedSmart) !== 0) { return 'failed' } else { return 'passed' @@ -181,138 +184,140 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { } - getAttributeName(attribute_data): string { - const attribute_metadata = this.metadata[attribute_data.attribute_id] - if(!attribute_metadata){ + getAttributeName(attributeData: SmartAttributeModel): string { + const attributeMetadata = this.metadata[attributeData.attribute_id] + if (!attributeMetadata) { return 'Unknown Attribute Name' } else { - return attribute_metadata.display_name + return attributeMetadata.display_name } } - getAttributeDescription(attribute_data){ - const attribute_metadata = this.metadata[attribute_data.attribute_id] - if(!attribute_metadata){ + + getAttributeDescription(attributeData: SmartAttributeModel): string { + const attributeMetadata = this.metadata[attributeData.attribute_id] + if (!attributeMetadata) { return 'Unknown' } else { - return attribute_metadata.description + return attributeMetadata.description } - return } - getAttributeValue(attribute_data){ - if(this.isAta()) { - const attribute_metadata = this.metadata[attribute_data.attribute_id] - if(!attribute_metadata){ - return attribute_data.value - } else if (attribute_metadata.display_type == 'raw') { - return attribute_data.raw_value - } else if (attribute_metadata.display_type == 'transformed' && attribute_data.transformed_value) { - return attribute_data.transformed_value + getAttributeValue(attributeData: SmartAttributeModel): number { + if (this.isAta()) { + const attributeMetadata = this.metadata[attributeData.attribute_id] + if (!attributeMetadata) { + return attributeData.value + } else if (attributeMetadata.display_type === 'raw') { + return attributeData.raw_value + } else if (attributeMetadata.display_type === 'transformed' && attributeData.transformed_value) { + return attributeData.transformed_value } else { - return attribute_data.value + return attributeData.value } - } - else{ - return attribute_data.value + } else { + return attributeData.value } } - getAttributeValueType(attribute_data){ - if(this.isAta()) { - const attribute_metadata = this.metadata[attribute_data.attribute_id] - if(!attribute_metadata){ + getAttributeValueType(attributeData: SmartAttributeModel): string { + if (this.isAta()) { + const attributeMetadata = this.metadata[attributeData.attribute_id] + if (!attributeMetadata) { return '' } else { - return attribute_metadata.display_type + return attributeMetadata.display_type } } else { return '' } } - getAttributeIdeal(attribute_data){ - if(this.isAta()){ - return this.metadata[attribute_data.attribute_id]?.display_type == 'raw' ? this.metadata[attribute_data.attribute_id]?.ideal : '' + getAttributeIdeal(attributeData: SmartAttributeModel): string { + if (this.isAta()) { + return this.metadata[attributeData.attribute_id]?.display_type === 'raw' ? this.metadata[attributeData.attribute_id]?.ideal : '' } else { - return this.metadata[attribute_data.attribute_id]?.ideal + return this.metadata[attributeData.attribute_id]?.ideal } } - getAttributeWorst(attribute_data){ - const attribute_metadata = this.metadata[attribute_data.attribute_id] - if(!attribute_metadata){ - return attribute_data.worst + getAttributeWorst(attributeData: SmartAttributeModel): number | string { + const attributeMetadata = this.metadata[attributeData.attribute_id] + if (!attributeMetadata) { + return attributeData.worst } else { - return attribute_metadata?.display_type == 'normalized' ? attribute_data.worst : '' + return attributeMetadata?.display_type === 'normalized' ? attributeData.worst : '' } } - getAttributeThreshold(attribute_data){ - if(this.isAta()){ - const attribute_metadata = this.metadata[attribute_data.attribute_id] - if(!attribute_metadata || attribute_metadata.display_type == 'normalized'){ - return attribute_data.thresh + getAttributeThreshold(attributeData: SmartAttributeModel): number | string { + if (this.isAta()) { + const attributeMetadata = this.metadata[attributeData.attribute_id] + if (!attributeMetadata || attributeMetadata.display_type === 'normalized') { + return attributeData.thresh } else { // if(this.data.metadata[attribute_data.attribute_id].observed_thresholds){ // // } else { // } // return '' - return attribute_data.thresh + return attributeData.thresh } } else { - return (attribute_data.thresh == -1 ? '' : attribute_data.thresh ) + return (attributeData.thresh === -1 ? '' : attributeData.thresh) } } - getAttributeCritical(attribute_data){ - return this.metadata[attribute_data.attribute_id]?.critical + getAttributeCritical(attributeData: SmartAttributeModel): boolean { + return this.metadata[attributeData.attribute_id]?.critical } - getHiddenAttributes(){ - if (!this.smart_results || this.smart_results.length == 0) { + + getHiddenAttributes(): number { + if (!this.smart_results || this.smart_results.length === 0) { return 0 } - let attributes_length = 0 + let attributesLength = 0 const attributes = this.smart_results[0]?.attrs if (attributes) { - attributes_length = Object.keys(attributes).length + attributesLength = Object.keys(attributes).length } - return attributes_length - this.smartAttributeDataSource.data.length + return attributesLength - this.smartAttributeDataSource.data.length } isAta(): boolean { - return this.device.device_protocol == 'ATA' + return this.device.device_protocol === 'ATA' } + isScsi(): boolean { - return this.device.device_protocol == 'SCSI' + return this.device.device_protocol === 'SCSI' } + isNvme(): boolean { - return this.device.device_protocol == 'NVMe' + return this.device.device_protocol === 'NVMe' } - private _generateSmartAttributeTableDataSource(smart_results){ - const smartAttributeDataSource = []; + private _generateSmartAttributeTableDataSource(smartResults: SmartModel[]): SmartAttributeModel[] { + const smartAttributeDataSource: SmartAttributeModel[] = []; - if(smart_results.length == 0){ + if (smartResults.length === 0) { return smartAttributeDataSource } - const latest_smart_result = smart_results[0]; - let attributes = {} - if(this.isScsi()) { + const latestSmartResult = smartResults[0]; + let attributes: { [p: string]: SmartAttributeModel } = {} + if (this.isScsi()) { this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'history']; - attributes = latest_smart_result.attrs - } else if(this.isNvme()){ + attributes = latestSmartResult.attrs + } else if (this.isNvme()) { this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'ideal', 'history']; - attributes = latest_smart_result.attrs + attributes = latestSmartResult.attrs } else { // ATA - attributes = latest_smart_result.attrs - this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'thresh','ideal', 'failure', 'history']; + attributes = latestSmartResult.attrs + this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'thresh', 'ideal', 'failure', 'history']; } - for(const attrId in attributes){ + for (const attrId in attributes) { const attr = attributes[attrId] // chart history data @@ -320,18 +325,18 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { const attrHistory = [] - for (const smart_result of smart_results){ + for (const smartResult of smartResults) { // attrHistory.push(this.getAttributeValue(smart_result.attrs[attrId])) const chartDatapoint = { - x: formatDate(smart_result.date, 'MMMM dd, yyyy - HH:mm', this.locale), - y: this.getAttributeValue(smart_result.attrs[attrId]) + x: formatDate(smartResult.date, 'MMMM dd, yyyy - HH:mm', this.locale), + y: this.getAttributeValue(smartResult.attrs[attrId]) } - const attributeStatusName = this.getAttributeStatusName(smart_result.attrs[attrId].status) - if(attributeStatusName === 'failed') { + const attributeStatusName = this.getAttributeStatusName(smartResult.attrs[attrId].status) + if (attributeStatusName === 'failed') { chartDatapoint['strokeColor'] = '#F05252' chartDatapoint['fillColor'] = '#F05252' - } else if (attributeStatusName === 'warn'){ + } else if (attributeStatusName === 'warn') { chartDatapoint['strokeColor'] = '#C27803' chartDatapoint['fillColor'] = '#C27803' } @@ -350,7 +355,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { } // determine when to include the attributes in table. - if(!this.onlyCritical || this.onlyCritical && this.metadata[attr.attribute_id]?.critical || attr.value < attr.thresh){ + if (!this.onlyCritical || this.onlyCritical && this.metadata[attr.attribute_id]?.critical || attr.value < attr.thresh) { smartAttributeDataSource.push(attr) } } @@ -362,8 +367,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { * * @private */ - private _prepareChartData(): void - { + private _prepareChartData(): void { // Account balance this.commonSparklineOptions = { @@ -392,7 +396,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { }, y: { title: { - formatter: function(seriesName) { + formatter: (seriesName) => { return ''; } } @@ -410,27 +414,28 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { }; } - private determineTheme(config:AppConfig): string { + private determineTheme(config: AppConfig): string { if (config.theme === 'system') { return this.systemPrefersDark ? 'dark' : 'light' } else { return config.theme } } + // ----------------------------------------------------------------------------------------------------- // @ Public methods // ----------------------------------------------------------------------------------------------------- - toHex(decimalNumb){ + toHex(decimalNumb: number | string): string { return '0x' + Number(decimalNumb).toString(16).padStart(2, '0').toUpperCase() } - toggleOnlyCritical(){ + + toggleOnlyCritical(): void { this.onlyCritical = !this.onlyCritical this.smartAttributeDataSource.data = this._generateSmartAttributeTableDataSource(this.smart_results); - } - openDialog() { + openDialog(): void { const dialogRef = this.dialog.open(DetailSettingsComponent); dialogRef.afterClosed().subscribe(result => { @@ -444,8 +449,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { * @param index * @param item */ - trackByFn(index: number, item: any): any - { + trackByFn(index: number, item: any): any { return index; // return item.id || index; } diff --git a/webapp/frontend/src/app/modules/detail/detail.resolvers.ts b/webapp/frontend/src/app/modules/detail/detail.resolvers.ts index b416a3d..221cad1 100644 --- a/webapp/frontend/src/app/modules/detail/detail.resolvers.ts +++ b/webapp/frontend/src/app/modules/detail/detail.resolvers.ts @@ -1,13 +1,13 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; -import { Observable } from 'rxjs'; -import { DetailService } from 'app/modules/detail/detail.service'; +import {Injectable} from '@angular/core'; +import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router'; +import {Observable} from 'rxjs'; +import {DetailService} from 'app/modules/detail/detail.service'; +import {DeviceDetailsResponseWrapper} from 'app/core/models/device-details-response-wrapper'; @Injectable({ providedIn: 'root' }) -export class DetailResolver implements Resolve -{ +export class DetailResolver implements Resolve { /** * Constructor * @@ -29,8 +29,7 @@ export class DetailResolver implements Resolve * @param route * @param state */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable - { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { return this._detailService.getData(route.params.wwn); } } diff --git a/webapp/frontend/src/app/modules/detail/detail.service.spec.ts b/webapp/frontend/src/app/modules/detail/detail.service.spec.ts new file mode 100644 index 0000000..ec6a4b9 --- /dev/null +++ b/webapp/frontend/src/app/modules/detail/detail.service.spec.ts @@ -0,0 +1,28 @@ +import {HttpClient} from '@angular/common/http'; +import {DetailService} from './detail.service'; +import {of} from 'rxjs'; +import {sda} from 'app/data/mock/device/details/sda' +import {DeviceDetailsResponseWrapper} from 'app/core/models/device-details-response-wrapper'; + +describe('DetailService', () => { + describe('#getData', () => { + let service: DetailService; + let httpClientSpy: jasmine.SpyObj; + + beforeEach(() => { + httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']); + service = new DetailService(httpClientSpy); + }); + it('should return getData() (HttpClient called once)', (done: DoneFn) => { + httpClientSpy.get.and.returnValue(of(sda)); + + service.getData('test').subscribe(value => { + expect(value).toBe(sda as DeviceDetailsResponseWrapper); + done(); + }); + expect(httpClientSpy.get.calls.count()) + .withContext('one call') + .toBe(1); + }); + }) +}); diff --git a/webapp/frontend/src/app/modules/detail/detail.service.ts b/webapp/frontend/src/app/modules/detail/detail.service.ts index 5747571..e75cb8b 100644 --- a/webapp/frontend/src/app/modules/detail/detail.service.ts +++ b/webapp/frontend/src/app/modules/detail/detail.service.ts @@ -1,16 +1,16 @@ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { getBasePath } from 'app/app.routing'; +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {BehaviorSubject, Observable} from 'rxjs'; +import {tap} from 'rxjs/operators'; +import {getBasePath} from 'app/app.routing'; +import {DeviceDetailsResponseWrapper} from 'app/core/models/device-details-response-wrapper'; @Injectable({ providedIn: 'root' }) -export class DetailService -{ +export class DetailService { // Observables - private _data: BehaviorSubject; + private _data: BehaviorSubject; /** * Constructor @@ -19,8 +19,7 @@ export class DetailService */ constructor( private _httpClient: HttpClient - ) - { + ) { // Set the private defaults this._data = new BehaviorSubject(null); } @@ -32,8 +31,7 @@ export class DetailService /** * Getter for data */ - get data$(): Observable - { + get data$(): Observable { return this._data.asObservable(); } @@ -44,10 +42,9 @@ export class DetailService /** * Get data */ - getData(wwn): Observable - { + getData(wwn): Observable { return this._httpClient.get(getBasePath() + `/api/device/${wwn}/details`).pipe( - tap((response: any) => { + tap((response: DeviceDetailsResponseWrapper) => { this._data.next(response); }) ); diff --git a/webapp/frontend/src/app/shared/device-title.pipe.spec.ts b/webapp/frontend/src/app/shared/device-title.pipe.spec.ts index 0661e84..992cf9a 100644 --- a/webapp/frontend/src/app/shared/device-title.pipe.spec.ts +++ b/webapp/frontend/src/app/shared/device-title.pipe.spec.ts @@ -1,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': { diff --git a/webapp/frontend/src/app/shared/device-title.pipe.ts b/webapp/frontend/src/app/shared/device-title.pipe.ts index 0c873f8..3cabc0f 100644 --- a/webapp/frontend/src/app/shared/device-title.pipe.ts +++ b/webapp/frontend/src/app/shared/device-title.pipe.ts @@ -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) }