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; + let component: DashboardDeviceDeleteDialogComponent; + let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ DashboardDeviceDeleteDialogComponent ] - }) - .compileComponents(); - })); + const matDialogRefSpy = jasmine.createSpyObj('MatDialogRef', ['closeDialog', 'close']); + const dashboardDeviceDeleteDialogServiceSpy = jasmine.createSpyObj('DashboardDeviceDeleteDialogService', ['deleteDevice']); - beforeEach(() => { - fixture = TestBed.createComponent(DashboardDeviceDeleteDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + 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() + })); - it('should create', () => { - expect(component).toBeTruthy(); - }); + 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) }