From 0e2fec4e93d019f40afa41312d60686069435564 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Fri, 8 Jul 2022 07:11:59 -0700 Subject: [PATCH 01/14] adding tests to frontend. --- webapp/frontend/angular.json | 17 ++- .../src/app/core/models/device-model.ts | 26 ++++ .../modules/detail/detail.component.spec.ts | 17 ++- .../src/app/shared/device-title.pipe.spec.ts | 144 ++++++++++++++++++ .../src/app/shared/device-title.pipe.ts | 3 +- .../src/app/shared/file-size.pipe.spec.ts | 35 +++++ .../src/app/shared/temperature.pipe.spec.ts | 83 +++++++++- 7 files changed, 316 insertions(+), 9 deletions(-) create mode 100644 webapp/frontend/src/app/core/models/device-model.ts create mode 100644 webapp/frontend/src/app/shared/file-size.pipe.spec.ts diff --git a/webapp/frontend/angular.json b/webapp/frontend/angular.json index 004e2cd..6ea760f 100644 --- a/webapp/frontend/angular.json +++ b/webapp/frontend/angular.json @@ -91,6 +91,7 @@ }, "test": { "builder": "@angular-devkit/build-angular:karma", + "defaultConfiguration": "production", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", @@ -101,10 +102,22 @@ "src/favicon-32x32.png", "src/assets" ], + "stylePreprocessorOptions": { + "includePaths": [ + "src/@treo/styles" + ] + }, "styles": [ - "src/styles.scss" + "src/styles/vendors.scss", + "src/@treo/styles/main.scss", + "src/styles/styles.scss", + "src/styles/tailwind.scss" ], - "scripts": [] + "scripts": [], + "fileReplacements": [{ + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + }] } }, "lint": { diff --git a/webapp/frontend/src/app/core/models/device-model.ts b/webapp/frontend/src/app/core/models/device-model.ts new file mode 100644 index 0000000..ae7e5b0 --- /dev/null +++ b/webapp/frontend/src/app/core/models/device-model.ts @@ -0,0 +1,26 @@ +// 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; + + manufacturer: string; + model_name: string; + interface_type: string; + interface_speed: string; + serial_number: string; + firmware: string; + rotational_speed: number; + capacity: number; + form_factor: string; + smart_support: boolean; + device_protocol: string; + device_type: string; + + label: string; + host_id: string; + + device_status: number; +} diff --git a/webapp/frontend/src/app/modules/detail/detail.component.spec.ts b/webapp/frontend/src/app/modules/detail/detail.component.spec.ts index 149b9be..6f956ee 100644 --- a/webapp/frontend/src/app/modules/detail/detail.component.spec.ts +++ b/webapp/frontend/src/app/modules/detail/detail.component.spec.ts @@ -1,14 +1,27 @@ 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({ - declarations: [ DetailComponent ] + imports: [ + HttpClientModule, + MatDialogModule + + ], + declarations: [ DetailComponent, DeviceTitlePipe ], + providers: [ TREO_APP_CONFIG_PROVIDER ] }) .compileComponents(); })); diff --git a/webapp/frontend/src/app/shared/device-title.pipe.spec.ts b/webapp/frontend/src/app/shared/device-title.pipe.spec.ts index 1d64103..0661e84 100644 --- a/webapp/frontend/src/app/shared/device-title.pipe.spec.ts +++ b/webapp/frontend/src/app/shared/device-title.pipe.spec.ts @@ -1,8 +1,152 @@ import { DeviceTitlePipe } from './device-title.pipe'; +import {FileSizePipe} from "./file-size.pipe"; +import {DeviceModel} from "../core/models/device-model"; describe('DeviceTitlePipe', () => { it('create an instance', () => { const pipe = new DeviceTitlePipe(); expect(pipe).toBeTruthy(); }); + + describe('#deviceTitleForType',() => { + const testCases = [ + { + 'device': { + 'device_name': 'sda', + 'device_type': 'ata', + 'model_name': 'Samsung', + }, + 'titleType': 'name', + 'result': '/dev/sda - Samsung' + },{ + 'device': { + 'device_name': 'nvme0', + 'device_type': 'nvme', + 'model_name': 'Samsung', + }, + 'titleType': 'name', + 'result': '/dev/nvme0 - nvme - Samsung' + },{ + 'device': {}, + 'titleType': 'serial_id', + 'result': '' + },{ + 'device': { + 'device_serial_id': 'ata-WDC_WD140EDFZ-11AXXXXX_9RXXXXXX', + }, + 'titleType': 'serial_id', + 'result': '/by-id/ata-WDC_WD140EDFZ-11AXXXXX_9RXXXXXX' + },{ + 'device': {}, + 'titleType': 'uuid', + 'result': '' + },{ + 'device': { + 'device_uuid': 'abcdef-1234-4567-8901' + }, + 'titleType': 'uuid', + 'result': '/by-uuid/abcdef-1234-4567-8901' + },{ + 'device': {}, + 'titleType': 'label', + 'result': '' + },{ + 'device': { + 'label': 'custom-device-label' + }, + 'titleType': 'label', + 'result': 'custom-device-label' + },{ + 'device': { + 'device_label': 'drive-volume-label' + }, + 'titleType': 'label', + 'result': '/by-label/drive-volume-label' + }, + ] + testCases.forEach((test, index) => { + it(`should correctly format device title ${JSON.stringify(test.device)}. (testcase: ${index + 1})`, () => { + // test + const formatted = DeviceTitlePipe.deviceTitleForType(test.device as DeviceModel, test.titleType) + expect(formatted).toEqual(test.result); + }); + }) + }) + + describe('#deviceTitleWithFallback',() => { + const testCases = [ + { + 'device': { + 'device_name': 'sda', + 'device_type': 'ata', + 'model_name': 'Samsung', + }, + 'titleType': 'name', + 'result': '/dev/sda - Samsung' + },{ + 'device': { + 'device_name': 'nvme0', + 'device_type': 'nvme', + 'model_name': 'Samsung', + }, + 'titleType': 'name', + 'result': '/dev/nvme0 - nvme - Samsung' + },{ + 'device': { + 'device_name': 'fallback', + 'device_type': 'ata', + 'model_name': 'fallback', + }, + 'titleType': 'serial_id', + 'result': '/dev/fallback - fallback' + },{ + 'device': { + 'device_serial_id': 'ata-WDC_WD140EDFZ-11AXXXXX_9RXXXXXX', + }, + 'titleType': 'serial_id', + 'result': '/by-id/ata-WDC_WD140EDFZ-11AXXXXX_9RXXXXXX' + },{ + 'device': { + 'device_name': 'fallback', + 'device_type': 'ata', + 'model_name': 'fallback', + }, + 'titleType': 'uuid', + 'result': '/dev/fallback - fallback' + },{ + 'device': { + 'device_uuid': 'abcdef-1234-4567-8901' + }, + 'titleType': 'uuid', + 'result': '/by-uuid/abcdef-1234-4567-8901' + },{ + 'device': { + 'device_name': 'fallback', + 'device_type': 'ata', + 'model_name': 'fallback', + }, + 'titleType': 'label', + 'result': '/dev/fallback - fallback' + },{ + 'device': { + 'label': 'custom-device-label' + }, + 'titleType': 'label', + 'result': 'custom-device-label' + },{ + 'device': { + 'device_label': 'drive-volume-label' + }, + 'titleType': 'label', + 'result': '/by-label/drive-volume-label' + }, + ] + testCases.forEach((test, index) => { + it(`should correctly format device title ${JSON.stringify(test.device)}. (testcase: ${index + 1})`, () => { + // test + const formatted = DeviceTitlePipe.deviceTitleWithFallback(test.device as DeviceModel, test.titleType) + expect(formatted).toEqual(test.result); + }); + }) + }) }); diff --git a/webapp/frontend/src/app/shared/device-title.pipe.ts b/webapp/frontend/src/app/shared/device-title.pipe.ts index 1196fb8..0c873f8 100644 --- a/webapp/frontend/src/app/shared/device-title.pipe.ts +++ b/webapp/frontend/src/app/shared/device-title.pipe.ts @@ -1,11 +1,12 @@ import { Pipe, PipeTransform } from '@angular/core'; +import {DeviceModel} from 'app/core/models/device-model'; @Pipe({ name: 'deviceTitle' }) export class DeviceTitlePipe implements PipeTransform { - static deviceTitleForType(device: any, titleType: string): string { + static deviceTitleForType(device: DeviceModel, titleType: string): string { const titleParts = [] switch(titleType){ case 'name': diff --git a/webapp/frontend/src/app/shared/file-size.pipe.spec.ts b/webapp/frontend/src/app/shared/file-size.pipe.spec.ts new file mode 100644 index 0000000..14973cf --- /dev/null +++ b/webapp/frontend/src/app/shared/file-size.pipe.spec.ts @@ -0,0 +1,35 @@ +import { FileSizePipe } from './file-size.pipe'; + +describe('FileSizePipe', () => { + it('create an instance', () => { + const pipe = new FileSizePipe(); + expect(pipe).toBeTruthy(); + }); + + describe('#transform',() => { + const testCases = [ + { + 'bytes': 1500, + 'precision': undefined, + 'result': '1 KB' + },{ + 'bytes': 2_100_000_000, + 'precision': undefined, + 'result': '2.0 GB', + },{ + 'bytes': 1500, + 'precision': 2, + 'result': '1.46 KB', + } + ] + testCases.forEach((test, index) => { + it(`should correctly format bytes ${test.bytes}. (testcase: ${index + 1})`, () => { + // test + const pipe = new FileSizePipe(); + const formatted = pipe.transform(test.bytes, test.precision) + expect(formatted).toEqual(test.result); + }); + }) + }) + +}); diff --git a/webapp/frontend/src/app/shared/temperature.pipe.spec.ts b/webapp/frontend/src/app/shared/temperature.pipe.spec.ts index fc30978..70a4908 100644 --- a/webapp/frontend/src/app/shared/temperature.pipe.spec.ts +++ b/webapp/frontend/src/app/shared/temperature.pipe.spec.ts @@ -1,8 +1,83 @@ import { TemperaturePipe } from './temperature.pipe'; describe('TemperaturePipe', () => { - it('create an instance', () => { - const pipe = new TemperaturePipe(); - expect(pipe).toBeTruthy(); - }); + it('create an instance', () => { + const pipe = new TemperaturePipe(); + expect(pipe).toBeTruthy(); + }); + + + describe('#celsiusToFahrenheit', () => { + const testCases = [ + { + 'c': -273.15, + 'f': -460, + },{ + 'c': -34.44, + 'f': -30, + },{ + 'c': -23.33, + 'f': -10, + },{ + 'c': -17.78, + 'f': -0, + },{ + 'c': 0, + 'f': 32, + },{ + 'c': 10, + 'f': 50, + },{ + 'c': 26.67, + 'f': 80, + },{ + 'c': 37, + 'f': 99, + },{ + 'c': 60, + 'f': 140, + } + ] + testCases.forEach((test, index) => { + it(`should correctly convert ${test.c}, Celsius to Fahrenheit (testcase: ${index + 1})`, () => { + // test + const numb = TemperaturePipe.celsiusToFahrenheit(test.c) + const roundNumb = Math.round(numb); + expect(roundNumb).toEqual(test.f); + }); + }) + }); + + describe('#formatTemperature',() => { + const testCases = [ + { + 'c': 26.67, + 'unit': 'celsius', + 'includeUnits': true, + 'result': '26.67°C' + },{ + 'c': 26.67, + 'unit': 'celsius', + 'includeUnits': false, + 'result': '26.67', + },{ + 'c': 26.67, + 'unit': 'fahrenheit', + 'includeUnits': true, + 'result': '80.006°F', + },{ + 'c': 26.67, + 'unit': 'fahrenheit', + 'includeUnits': false, + 'result': '80.006', + } + ] + testCases.forEach((test, index) => { + it(`should correctly format temperature ${test.c} to ${test.unit} ${test.includeUnits ? 'with' : 'without'} unit. (testcase: ${index + 1})`, () => { + // test + const formatted = TemperaturePipe.formatTemperature(test.c, test.unit, test.includeUnits) + expect(formatted).toEqual(test.result); + }); + }) + }) }); From b71d6660a6a67b6d51a067b63f01775a2cf712da Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Fri, 8 Jul 2022 18:19:07 -0700 Subject: [PATCH 02/14] adding typescript interfaces for type hinting and testing some code reformatting adding tests for services and components. cleanup of unused dependencies in components. refactor dashboard service so that wrapper is removed before data is passed to component. (no more this.data.data...). refactored components so that variable names are consistent (dashboardService vs smartService). ensure argument and return types are specified everywhere. adding tests for pipes. adding ng test to ci steps. change dir before running npm install. trying to install nodejs in continer. test frontend separately. upload coverage for frontend and backend. upload coverage for frontend and backend. testing coverage file locations. retry file upload. --- .github/workflows/ci.yaml | 47 +- Makefile | 10 + .../pkg/web/handler/get_devices_summary.go | 1 + webapp/frontend/.gitignore | 2 + webapp/frontend/karma.conf.js | 4 +- .../src/app/core/config/app.config.ts | 18 +- .../models/device-details-response-wrapper.ts | 14 + .../src/app/core/models/device-model.ts | 8 +- .../app/core/models/device-summary-model.ts | 16 + .../models/device-summary-response-wrapper.ts | 10 + .../device-summary-temp-response-wrapper.ts | 9 + .../measurements/smart-attribute-model.ts | 19 + .../core/models/measurements/smart-model.ts | 13 + .../measurements/smart-temperature-model.ts | 6 + .../thresholds/attribute-metadata-model.ts | 13 + .../src/app/data/mock/summary/temp_history.ts | 1200 +++++++++++++++++ ...ard-device-delete-dialog.component.spec.ts | 81 +- ...ashboard-device-delete-dialog.component.ts | 3 +- .../dashboard-device-delete-dialog.module.ts | 37 +- .../dashboard-device.component.spec.ts | 118 +- .../dashboard-device.component.ts | 37 +- .../dashboard-device.module.ts | 41 +- .../dashboard-settings.component.spec.ts | 25 - .../dashboard-settings.component.ts | 40 +- .../detail-settings/detail-settings.module.ts | 32 +- .../dashboard/dashboard.component.html | 2 +- .../modules/dashboard/dashboard.component.ts | 70 +- .../modules/dashboard/dashboard.resolvers.ts | 15 +- .../dashboard/dashboard.service.spec.ts | 44 + .../modules/dashboard/dashboard.service.ts | 42 +- .../modules/detail/detail.component.spec.ts | 38 - .../app/modules/detail/detail.component.ts | 240 ++-- .../app/modules/detail/detail.resolvers.ts | 15 +- .../app/modules/detail/detail.service.spec.ts | 28 + .../src/app/modules/detail/detail.service.ts | 27 +- .../src/app/shared/device-title.pipe.spec.ts | 15 +- .../src/app/shared/device-title.pipe.ts | 6 +- 37 files changed, 1897 insertions(+), 449 deletions(-) create mode 100644 webapp/frontend/src/app/core/models/device-details-response-wrapper.ts create mode 100644 webapp/frontend/src/app/core/models/device-summary-model.ts create mode 100644 webapp/frontend/src/app/core/models/device-summary-response-wrapper.ts create mode 100644 webapp/frontend/src/app/core/models/device-summary-temp-response-wrapper.ts create mode 100644 webapp/frontend/src/app/core/models/measurements/smart-attribute-model.ts create mode 100644 webapp/frontend/src/app/core/models/measurements/smart-model.ts create mode 100644 webapp/frontend/src/app/core/models/measurements/smart-temperature-model.ts create mode 100644 webapp/frontend/src/app/core/models/thresholds/attribute-metadata-model.ts create mode 100644 webapp/frontend/src/app/data/mock/summary/temp_history.ts delete mode 100644 webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.spec.ts create mode 100644 webapp/frontend/src/app/modules/dashboard/dashboard.service.spec.ts delete mode 100644 webapp/frontend/src/app/modules/detail/detail.component.spec.ts create mode 100644 webapp/frontend/src/app/modules/detail/detail.service.spec.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1310337..233fa26 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,11 +3,25 @@ name: CI on: [pull_request] jobs: - test: - name: Test + test-frontend: + name: Test Frontend + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Test Frontend + run: | + make binary-frontend-test-coverage + - name: Upload coverage + uses: actions/upload-artifact@v3 + with: + name: coverage + path: ${{ github.workspace }}/webapp/frontend/coverage/lcov.info + retention-days: 1 + test-backend: + name: Test Backend runs-on: ubuntu-latest container: ghcr.io/packagrio/packagr:latest-golang - # Service containers to run with `build` (Required for end-to-end testing) services: influxdb: @@ -22,7 +36,6 @@ jobs: ports: - 8086:8086 env: - PROJECT_PATH: /go/src/github.com/analogj/scrutiny STATIC: true steps: - name: Git @@ -32,16 +45,36 @@ jobs: git --version - name: Checkout uses: actions/checkout@v2 - - name: Test + - name: Test Backend run: | make binary-clean binary-test-coverage - - name: Generate coverage report + - name: Upload coverage + uses: actions/upload-artifact@v3 + with: + name: coverage + path: ${{ github.workspace }}/coverage.txt + retention-days: 1 + test-coverage: + name: Test Coverage Upload + needs: + - test-backend + - test-frontend + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Download coverage reports + uses: actions/download-artifact@v3 + with: + name: coverage + - name: Upload coverage reports uses: codecov/codecov-action@v2 with: - files: ${{ github.workspace }}/coverage.txt + files: ${{ github.workspace }}/coverage.txt,${{ github.workspace }}/lcov.info flags: unittests fail_ci_if_error: true verbose: true + build: name: Build ${{ matrix.cfg.goos }}/${{ matrix.cfg.goarch }} runs-on: ${{ matrix.cfg.on }} diff --git a/Makefile b/Makefile index afb2d84..176d325 100644 --- a/Makefile +++ b/Makefile @@ -89,6 +89,10 @@ ifneq ($(OS),Windows_NT) ./$(WEB_BINARY_NAME) || true endif +######################################################################################################################## +# Binary +######################################################################################################################## + .PHONY: binary-frontend # reduce logging, disable angular-cli analytics for ci environment binary-frontend: export NPM_CONFIG_LOGLEVEL = warn @@ -100,6 +104,12 @@ binary-frontend: npm ci npm run build:prod -- --output-path=$(CURDIR)/dist +.PHONY: binary-frontend-test-coverage +# reduce logging, disable angular-cli analytics for ci environment +binary-frontend-test-coverage: + cd webapp/frontend + npm ci + npx ng test --watch=false --browsers=ChromeHeadless --code-coverage ######################################################################################################################## # Docker diff --git a/webapp/backend/pkg/web/handler/get_devices_summary.go b/webapp/backend/pkg/web/handler/get_devices_summary.go index 8eb392f..56e3eb5 100644 --- a/webapp/backend/pkg/web/handler/get_devices_summary.go +++ b/webapp/backend/pkg/web/handler/get_devices_summary.go @@ -18,6 +18,7 @@ func GetDevicesSummary(c *gin.Context) { return } + //this must match DeviceSummaryWrapper (webapp/backend/pkg/models/device_summary.go) c.JSON(http.StatusOK, gin.H{ "success": true, "data": map[string]interface{}{ diff --git a/webapp/frontend/.gitignore b/webapp/frontend/.gitignore index 10fbf55..dd9d262 100644 --- a/webapp/frontend/.gitignore +++ b/webapp/frontend/.gitignore @@ -46,3 +46,5 @@ testem.log Thumbs.db /dist + +/coverage diff --git a/webapp/frontend/karma.conf.js b/webapp/frontend/karma.conf.js index 04347ed..4ccca41 100644 --- a/webapp/frontend/karma.conf.js +++ b/webapp/frontend/karma.conf.js @@ -17,8 +17,8 @@ module.exports = function (config) clearContext: false // leave Jasmine Spec Runner output visible in browser }, coverageIstanbulReporter: { - dir : require('path').join(__dirname, './coverage/treo'), - reports : ['html', 'lcovonly', 'text-summary'], + dir: require('path').join(__dirname, './coverage'), + reports: ['html', 'lcovonly', 'text-summary'], fixWebpackSourcePaths: true }, reporters : ['progress', 'kjhtml'], diff --git a/webapp/frontend/src/app/core/config/app.config.ts b/webapp/frontend/src/app/core/config/app.config.ts index e98a6c8..74143c5 100644 --- a/webapp/frontend/src/app/core/config/app.config.ts +++ b/webapp/frontend/src/app/core/config/app.config.ts @@ -1,22 +1,28 @@ -import { Layout } from 'app/layout/layout.types'; +import {Layout} from 'app/layout/layout.types'; // Theme type export type Theme = 'light' | 'dark' | 'system'; +// Device title to display on the dashboard +export type DashboardDisplay = 'name' | 'serial_id' | 'uuid' | 'label' + +export type DashboardSort = 'status' | 'title' | 'age' + +export type TemperatureUnit = 'celsius' | 'fahrenheit' + /** * AppConfig interface. Update this interface to strictly type your config * object. */ -export interface AppConfig -{ +export interface AppConfig { theme: Theme; layout: Layout; // Dashboard options - dashboardDisplay: string; - dashboardSort: string; + dashboardDisplay: DashboardDisplay; + dashboardSort: DashboardSort; - temperatureUnit: string; + temperatureUnit: TemperatureUnit; } /** diff --git a/webapp/frontend/src/app/core/models/device-details-response-wrapper.ts b/webapp/frontend/src/app/core/models/device-details-response-wrapper.ts new file mode 100644 index 0000000..610af90 --- /dev/null +++ b/webapp/frontend/src/app/core/models/device-details-response-wrapper.ts @@ -0,0 +1,14 @@ +import {DeviceModel} from 'app/core/models/device-model'; +import {SmartModel} from 'app/core/models/measurements/smart-model'; +import {AttributeMetadataModel} from 'app/core/models/thresholds/attribute-metadata-model'; + +// maps to webapp/backend/pkg/models/device_summary.go +export interface DeviceDetailsResponseWrapper { + success: boolean; + errors?: any[]; + data: { + device: DeviceModel; + smart_results: SmartModel[]; + }, + metadata: { [key: string]: AttributeMetadataModel } | { [key: number]: AttributeMetadataModel }; +} diff --git a/webapp/frontend/src/app/core/models/device-model.ts b/webapp/frontend/src/app/core/models/device-model.ts index ae7e5b0..7613c3f 100644 --- a/webapp/frontend/src/app/core/models/device-model.ts +++ b/webapp/frontend/src/app/core/models/device-model.ts @@ -1,10 +1,10 @@ // maps to webapp/backend/pkg/models/device.go export interface DeviceModel { wwn: string; - device_name: string; - device_uuid: string; - device_serial_id: string; - device_label: string; + device_name?: string; + device_uuid?: string; + device_serial_id?: string; + device_label?: string; manufacturer: string; model_name: string; diff --git a/webapp/frontend/src/app/core/models/device-summary-model.ts b/webapp/frontend/src/app/core/models/device-summary-model.ts new file mode 100644 index 0000000..daf7ee4 --- /dev/null +++ b/webapp/frontend/src/app/core/models/device-summary-model.ts @@ -0,0 +1,16 @@ +import {DeviceModel} from 'app/core/models/device-model'; +import {SmartTemperatureModel} from 'app/core/models/measurements/smart-temperature-model'; + +// maps to webapp/backend/pkg/models/device_summary.go +export interface DeviceSummaryModel { + device: DeviceModel; + smart?: SmartSummary; + temp_history?: SmartTemperatureModel[]; +} + +export interface SmartSummary { + collector_date?: string, + temp?: number + power_on_hours?: number +} + diff --git a/webapp/frontend/src/app/core/models/device-summary-response-wrapper.ts b/webapp/frontend/src/app/core/models/device-summary-response-wrapper.ts new file mode 100644 index 0000000..cffac38 --- /dev/null +++ b/webapp/frontend/src/app/core/models/device-summary-response-wrapper.ts @@ -0,0 +1,10 @@ +import {DeviceSummaryModel} from 'app/core/models/device-summary-model'; + +// maps to webapp/backend/pkg/models/device_summary.go +export interface DeviceSummaryResponseWrapper { + success: boolean; + errors: any[]; + data: { + summary: { [key: string]: DeviceSummaryModel } + } +} diff --git a/webapp/frontend/src/app/core/models/device-summary-temp-response-wrapper.ts b/webapp/frontend/src/app/core/models/device-summary-temp-response-wrapper.ts new file mode 100644 index 0000000..c234fdd --- /dev/null +++ b/webapp/frontend/src/app/core/models/device-summary-temp-response-wrapper.ts @@ -0,0 +1,9 @@ +import {SmartTemperatureModel} from './measurements/smart-temperature-model'; + +export interface DeviceSummaryTempResponseWrapper { + success: boolean; + errors: any[]; + data: { + temp_history: { [key: string]: SmartTemperatureModel[]; } + } +} diff --git a/webapp/frontend/src/app/core/models/measurements/smart-attribute-model.ts b/webapp/frontend/src/app/core/models/measurements/smart-attribute-model.ts new file mode 100644 index 0000000..3a253b7 --- /dev/null +++ b/webapp/frontend/src/app/core/models/measurements/smart-attribute-model.ts @@ -0,0 +1,19 @@ +// maps to webapp/backend/pkg/models/measurements/smart_ata_attribute.go +// maps to webapp/backend/pkg/models/measurements/smart_nvme_attribute.go +// maps to webapp/backend/pkg/models/measurements/smart_scsi_attribute.go +export interface SmartAttributeModel { + attribute_id: number | string + value: number + thresh: number + worst?: number + raw_value?: number + raw_string?: string + when_failed?: string + + transformed_value: number + status: number + status_reason?: string + failure_rate?: number + + chartData?: any[] +} diff --git a/webapp/frontend/src/app/core/models/measurements/smart-model.ts b/webapp/frontend/src/app/core/models/measurements/smart-model.ts new file mode 100644 index 0000000..d44d2e1 --- /dev/null +++ b/webapp/frontend/src/app/core/models/measurements/smart-model.ts @@ -0,0 +1,13 @@ +// maps to webapp/backend/pkg/models/measurements/smart.go +import {SmartAttributeModel} from './smart-attribute-model'; + +export interface SmartModel { + date: string; + device_wwn: string; + device_protocol: string; + + temp: number; + power_on_hours: number; + power_cycle_count: number + attrs: { [key: string]: SmartAttributeModel } +} diff --git a/webapp/frontend/src/app/core/models/measurements/smart-temperature-model.ts b/webapp/frontend/src/app/core/models/measurements/smart-temperature-model.ts new file mode 100644 index 0000000..3b05313 --- /dev/null +++ b/webapp/frontend/src/app/core/models/measurements/smart-temperature-model.ts @@ -0,0 +1,6 @@ +// maps to webapp/backend/pkg/models/measurements/smart_temperature.go +export interface SmartTemperatureModel { + date: string; + temp: number; +} + diff --git a/webapp/frontend/src/app/core/models/thresholds/attribute-metadata-model.ts b/webapp/frontend/src/app/core/models/thresholds/attribute-metadata-model.ts new file mode 100644 index 0000000..02a4e94 --- /dev/null +++ b/webapp/frontend/src/app/core/models/thresholds/attribute-metadata-model.ts @@ -0,0 +1,13 @@ +// map to webapp/backend/pkg/thresholds/ata_attribute_metadata.go +// map to webapp/backend/pkg/thresholds/nvme_attribute_metadata.go +// map to webapp/backend/pkg/thresholds/scsi_attribute_metadata.go +export interface AttributeMetadataModel { + display_name: string + ideal: string + critical: boolean + description: string + + transform_value_unit?: string + observed_thresholds?: any[] + display_type: string +} diff --git a/webapp/frontend/src/app/data/mock/summary/temp_history.ts b/webapp/frontend/src/app/data/mock/summary/temp_history.ts new file mode 100644 index 0000000..80e6b45 --- /dev/null +++ b/webapp/frontend/src/app/data/mock/summary/temp_history.ts @@ -0,0 +1,1200 @@ +/* tslint:disable */ +export const temp_history = { + "data": { + "temp_history": { + "0x5000cca252c859cc": [{ + "date": "2022-07-01T22:00:00Z", + "temp": 36 + }, { + "date": "2022-07-01T23:00:00Z", + "temp": 36 + }, { + "date": "2022-07-02T00:00:00Z", + "temp": 36 + }, { + "date": "2022-07-02T01:00:00Z", + "temp": 36 + }, { + "date": "2022-07-02T22:00:00Z", + "temp": 36 + }, { + "date": "2022-07-02T23:00:00Z", + "temp": 36 + }, { + "date": "2022-07-03T00:00:00Z", + "temp": 36 + }, { + "date": "2022-07-03T01:00:00Z", + "temp": 36 + }, { + "date": "2022-07-03T22:00:00Z", + "temp": 42 + }, { + "date": "2022-07-03T23:00:00Z", + "temp": 42 + }, { + "date": "2022-07-04T00:00:00Z", + "temp": 42 + }, { + "date": "2022-07-04T01:00:00Z", + "temp": 42 + }, { + "date": "2022-07-04T22:00:00Z", + "temp": 37 + }, { + "date": "2022-07-04T23:00:00Z", + "temp": 36 + }, { + "date": "2022-07-05T00:00:00Z", + "temp": 36 + }, { + "date": "2022-07-05T01:00:00Z", + "temp": 37 + }, { + "date": "2022-07-05T22:00:00Z", + "temp": 37 + }, { + "date": "2022-07-05T23:00:00Z", + "temp": 37 + }, { + "date": "2022-07-06T00:00:00Z", + "temp": 37 + }, { + "date": "2022-07-06T01:00:00Z", + "temp": 37 + }, { + "date": "2022-07-06T04:00:00Z", + "temp": 37 + }, { + "date": "2022-07-06T05:00:00Z", + "temp": 37 + }, { + "date": "2022-07-06T06:00:00Z", + "temp": 37 + }, { + "date": "2022-07-06T22:00:00Z", + "temp": 38 + }, { + "date": "2022-07-06T23:00:00Z", + "temp": 38 + }, { + "date": "2022-07-07T00:00:00Z", + "temp": 38 + }, { + "date": "2022-07-07T01:00:00Z", + "temp": 38 + }, { + "date": "2022-07-07T03:00:00Z", + "temp": 37 + }, { + "date": "2022-07-07T04:00:00Z", + "temp": 37 + }, { + "date": "2022-07-07T05:00:00Z", + "temp": 37 + }, { + "date": "2022-07-07T06:00:00Z", + "temp": 37 + }, { + "date": "2022-07-07T07:00:00Z", + "temp": 38 + }, { + "date": "2022-07-07T22:00:00Z", + "temp": 37 + }, { + "date": "2022-07-07T23:00:00Z", + "temp": 37 + }, { + "date": "2022-07-08T00:00:00Z", + "temp": 37 + }, { + "date": "2022-07-08T01:00:00Z", + "temp": 37 + }, { + "date": "2022-07-08T13:00:00Z", + "temp": 35 + }, { + "date": "2022-07-08T14:00:00Z", + "temp": 35 + }, { + "date": "2022-07-08T15:00:00Z", + "temp": 35 + }], + "0x5000cca264eb01d7": [{ + "date": "2022-07-01T22:00:00Z", + "temp": 42 + }, { + "date": "2022-07-01T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-02T00:00:00Z", + "temp": 39 + }, { + "date": "2022-07-02T01:00:00Z", + "temp": 39 + }, { + "date": "2022-07-02T22:00:00Z", + "temp": 41 + }, { + "date": "2022-07-02T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-03T00:00:00Z", + "temp": 39 + }, { + "date": "2022-07-03T01:00:00Z", + "temp": 40 + }, { + "date": "2022-07-03T22:00:00Z", + "temp": 44 + }, { + "date": "2022-07-03T23:00:00Z", + "temp": 42 + }, { + "date": "2022-07-04T00:00:00Z", + "temp": 41 + }, { + "date": "2022-07-04T01:00:00Z", + "temp": 41 + }, { + "date": "2022-07-04T22:00:00Z", + "temp": 41 + }, { + "date": "2022-07-04T23:00:00Z", + "temp": 42 + }, { + "date": "2022-07-05T00:00:00Z", + "temp": 40 + }, { + "date": "2022-07-05T01:00:00Z", + "temp": 41 + }, { + "date": "2022-07-05T22:00:00Z", + "temp": 40 + }, { + "date": "2022-07-05T23:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T00:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T01:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T04:00:00Z", + "temp": 42 + }, { + "date": "2022-07-06T05:00:00Z", + "temp": 45 + }, { + "date": "2022-07-06T06:00:00Z", + "temp": 44 + }, { + "date": "2022-07-06T22:00:00Z", + "temp": 41 + }, { + "date": "2022-07-06T23:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T00:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T01:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T03:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T04:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T05:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T06:00:00Z", + "temp": 42 + }, { + "date": "2022-07-07T07:00:00Z", + "temp": 42 + }, { + "date": "2022-07-07T22:00:00Z", + "temp": 40 + }, { + "date": "2022-07-07T23:00:00Z", + "temp": 40 + }, { + "date": "2022-07-08T00:00:00Z", + "temp": 40 + }, { + "date": "2022-07-08T01:00:00Z", + "temp": 40 + }, { + "date": "2022-07-08T13:00:00Z", + "temp": 39 + }, { + "date": "2022-07-08T14:00:00Z", + "temp": 39 + }, { + "date": "2022-07-08T15:00:00Z", + "temp": 39 + }], + "0x5000cca264ebc248": [{ + "date": "2022-07-01T22:00:00Z", + "temp": 36 + }, { + "date": "2022-07-01T23:00:00Z", + "temp": 34 + }, { + "date": "2022-07-02T00:00:00Z", + "temp": 33 + }, { + "date": "2022-07-02T01:00:00Z", + "temp": 33 + }, { + "date": "2022-07-02T22:00:00Z", + "temp": 35 + }, { + "date": "2022-07-02T23:00:00Z", + "temp": 34 + }, { + "date": "2022-07-03T00:00:00Z", + "temp": 33 + }, { + "date": "2022-07-03T01:00:00Z", + "temp": 33 + }, { + "date": "2022-07-03T22:00:00Z", + "temp": 37 + }, { + "date": "2022-07-03T23:00:00Z", + "temp": 37 + }, { + "date": "2022-07-04T00:00:00Z", + "temp": 33 + }, { + "date": "2022-07-04T01:00:00Z", + "temp": 33 + }, { + "date": "2022-07-04T22:00:00Z", + "temp": 35 + }, { + "date": "2022-07-04T23:00:00Z", + "temp": 35 + }, { + "date": "2022-07-05T00:00:00Z", + "temp": 35 + }, { + "date": "2022-07-05T01:00:00Z", + "temp": 35 + }, { + "date": "2022-07-05T22:00:00Z", + "temp": 36 + }, { + "date": "2022-07-05T23:00:00Z", + "temp": 35 + }, { + "date": "2022-07-06T00:00:00Z", + "temp": 35 + }, { + "date": "2022-07-06T01:00:00Z", + "temp": 36 + }, { + "date": "2022-07-06T04:00:00Z", + "temp": 38 + }, { + "date": "2022-07-06T05:00:00Z", + "temp": 39 + }, { + "date": "2022-07-06T06:00:00Z", + "temp": 38 + }, { + "date": "2022-07-06T22:00:00Z", + "temp": 35 + }, { + "date": "2022-07-06T23:00:00Z", + "temp": 35 + }, { + "date": "2022-07-07T00:00:00Z", + "temp": 35 + }, { + "date": "2022-07-07T01:00:00Z", + "temp": 35 + }, { + "date": "2022-07-07T03:00:00Z", + "temp": 38 + }, { + "date": "2022-07-07T04:00:00Z", + "temp": 36 + }, { + "date": "2022-07-07T05:00:00Z", + "temp": 35 + }, { + "date": "2022-07-07T06:00:00Z", + "temp": 37 + }, { + "date": "2022-07-07T07:00:00Z", + "temp": 36 + }, { + "date": "2022-07-07T22:00:00Z", + "temp": 34 + }, { + "date": "2022-07-07T23:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T00:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T01:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T13:00:00Z", + "temp": 33 + }, { + "date": "2022-07-08T14:00:00Z", + "temp": 33 + }, { + "date": "2022-07-08T15:00:00Z", + "temp": 33 + }], + "0x5000cca264ec3183": [{ + "date": "2022-07-01T22:00:00Z", + "temp": 39 + }, { + "date": "2022-07-01T23:00:00Z", + "temp": 38 + }, { + "date": "2022-07-02T00:00:00Z", + "temp": 37 + }, { + "date": "2022-07-02T01:00:00Z", + "temp": 37 + }, { + "date": "2022-07-02T22:00:00Z", + "temp": 39 + }, { + "date": "2022-07-02T23:00:00Z", + "temp": 37 + }, { + "date": "2022-07-03T00:00:00Z", + "temp": 39 + }, { + "date": "2022-07-03T01:00:00Z", + "temp": 40 + }, { + "date": "2022-07-03T22:00:00Z", + "temp": 40 + }, { + "date": "2022-07-03T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-04T00:00:00Z", + "temp": 38 + }, { + "date": "2022-07-04T01:00:00Z", + "temp": 38 + }, { + "date": "2022-07-04T22:00:00Z", + "temp": 38 + }, { + "date": "2022-07-04T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-05T00:00:00Z", + "temp": 38 + }, { + "date": "2022-07-05T01:00:00Z", + "temp": 38 + }, { + "date": "2022-07-05T22:00:00Z", + "temp": 38 + }, { + "date": "2022-07-05T23:00:00Z", + "temp": 38 + }, { + "date": "2022-07-06T00:00:00Z", + "temp": 38 + }, { + "date": "2022-07-06T01:00:00Z", + "temp": 38 + }, { + "date": "2022-07-06T04:00:00Z", + "temp": 41 + }, { + "date": "2022-07-06T05:00:00Z", + "temp": 42 + }, { + "date": "2022-07-06T06:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T22:00:00Z", + "temp": 39 + }, { + "date": "2022-07-06T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-07T00:00:00Z", + "temp": 39 + }, { + "date": "2022-07-07T01:00:00Z", + "temp": 39 + }, { + "date": "2022-07-07T03:00:00Z", + "temp": 40 + }, { + "date": "2022-07-07T04:00:00Z", + "temp": 40 + }, { + "date": "2022-07-07T05:00:00Z", + "temp": 39 + }, { + "date": "2022-07-07T06:00:00Z", + "temp": 40 + }, { + "date": "2022-07-07T07:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T22:00:00Z", + "temp": 38 + }, { + "date": "2022-07-07T23:00:00Z", + "temp": 38 + }, { + "date": "2022-07-08T00:00:00Z", + "temp": 38 + }, { + "date": "2022-07-08T01:00:00Z", + "temp": 38 + }, { + "date": "2022-07-08T13:00:00Z", + "temp": 37 + }, { + "date": "2022-07-08T14:00:00Z", + "temp": 37 + }, { + "date": "2022-07-08T15:00:00Z", + "temp": 37 + }], + "0x5000cca28ed7fcd8": [{ + "date": "2022-07-01T22:00:00Z", + "temp": 36 + }, { + "date": "2022-07-01T23:00:00Z", + "temp": 34 + }, { + "date": "2022-07-02T00:00:00Z", + "temp": 34 + }, { + "date": "2022-07-02T01:00:00Z", + "temp": 34 + }, { + "date": "2022-07-02T22:00:00Z", + "temp": 35 + }, { + "date": "2022-07-02T23:00:00Z", + "temp": 34 + }, { + "date": "2022-07-03T00:00:00Z", + "temp": 33 + }, { + "date": "2022-07-03T01:00:00Z", + "temp": 33 + }, { + "date": "2022-07-03T22:00:00Z", + "temp": 36 + }, { + "date": "2022-07-03T23:00:00Z", + "temp": 36 + }, { + "date": "2022-07-04T00:00:00Z", + "temp": 33 + }, { + "date": "2022-07-04T01:00:00Z", + "temp": 33 + }, { + "date": "2022-07-04T22:00:00Z", + "temp": 39 + }, { + "date": "2022-07-04T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-05T00:00:00Z", + "temp": 39 + }, { + "date": "2022-07-05T01:00:00Z", + "temp": 39 + }, { + "date": "2022-07-05T22:00:00Z", + "temp": 40 + }, { + "date": "2022-07-05T23:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T00:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T01:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T04:00:00Z", + "temp": 38 + }, { + "date": "2022-07-06T05:00:00Z", + "temp": 38 + }, { + "date": "2022-07-06T06:00:00Z", + "temp": 36 + }, { + "date": "2022-07-06T22:00:00Z", + "temp": 36 + }, { + "date": "2022-07-06T23:00:00Z", + "temp": 35 + }, { + "date": "2022-07-07T00:00:00Z", + "temp": 36 + }, { + "date": "2022-07-07T01:00:00Z", + "temp": 36 + }, { + "date": "2022-07-07T03:00:00Z", + "temp": 38 + }, { + "date": "2022-07-07T04:00:00Z", + "temp": 37 + }, { + "date": "2022-07-07T05:00:00Z", + "temp": 36 + }, { + "date": "2022-07-07T06:00:00Z", + "temp": 39 + }, { + "date": "2022-07-07T07:00:00Z", + "temp": 37 + }, { + "date": "2022-07-07T22:00:00Z", + "temp": 34 + }, { + "date": "2022-07-07T23:00:00Z", + "temp": 35 + }, { + "date": "2022-07-08T00:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T01:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T13:00:00Z", + "temp": 33 + }, { + "date": "2022-07-08T14:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T15:00:00Z", + "temp": 34 + }], + "0x5000cca28fc25581": [{ + "date": "2022-07-01T22:00:00Z", + "temp": 39 + }, { + "date": "2022-07-01T23:00:00Z", + "temp": 38 + }, { + "date": "2022-07-02T00:00:00Z", + "temp": 38 + }, { + "date": "2022-07-02T01:00:00Z", + "temp": 38 + }, { + "date": "2022-07-02T22:00:00Z", + "temp": 39 + }, { + "date": "2022-07-02T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-03T00:00:00Z", + "temp": 39 + }, { + "date": "2022-07-03T01:00:00Z", + "temp": 39 + }, { + "date": "2022-07-03T22:00:00Z", + "temp": 46 + }, { + "date": "2022-07-03T23:00:00Z", + "temp": 46 + }, { + "date": "2022-07-04T00:00:00Z", + "temp": 46 + }, { + "date": "2022-07-04T01:00:00Z", + "temp": 46 + }, { + "date": "2022-07-04T22:00:00Z", + "temp": 40 + }, { + "date": "2022-07-04T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-05T00:00:00Z", + "temp": 39 + }, { + "date": "2022-07-05T01:00:00Z", + "temp": 41 + }, { + "date": "2022-07-05T22:00:00Z", + "temp": 40 + }, { + "date": "2022-07-05T23:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T00:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T01:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T04:00:00Z", + "temp": 41 + }, { + "date": "2022-07-06T05:00:00Z", + "temp": 41 + }, { + "date": "2022-07-06T06:00:00Z", + "temp": 41 + }, { + "date": "2022-07-06T22:00:00Z", + "temp": 40 + }, { + "date": "2022-07-06T23:00:00Z", + "temp": 40 + }, { + "date": "2022-07-07T00:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T01:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T03:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T04:00:00Z", + "temp": 40 + }, { + "date": "2022-07-07T05:00:00Z", + "temp": 40 + }, { + "date": "2022-07-07T06:00:00Z", + "temp": 41 + }, { + "date": "2022-07-07T07:00:00Z", + "temp": 40 + }, { + "date": "2022-07-07T22:00:00Z", + "temp": 39 + }, { + "date": "2022-07-07T23:00:00Z", + "temp": 39 + }, { + "date": "2022-07-08T00:00:00Z", + "temp": 39 + }, { + "date": "2022-07-08T01:00:00Z", + "temp": 39 + }, { + "date": "2022-07-08T13:00:00Z", + "temp": 38 + }, { + "date": "2022-07-08T14:00:00Z", + "temp": 38 + }, { + "date": "2022-07-08T15:00:00Z", + "temp": 38 + }], + "0x5002538e40a22954": [{ + "date": "2022-07-01T19:00:00Z", + "temp": 30 + }, { + "date": "2022-07-01T20:00:00Z", + "temp": 31 + }, { + "date": "2022-07-01T21:00:00Z", + "temp": 31 + }, { + "date": "2022-07-01T22:00:00Z", + "temp": 31 + }, { + "date": "2022-07-01T23:00:00Z", + "temp": 30 + }, { + "date": "2022-07-02T00:00:00Z", + "temp": 30 + }, { + "date": "2022-07-02T01:00:00Z", + "temp": 31 + }, { + "date": "2022-07-02T03:00:00Z", + "temp": 31 + }, { + "date": "2022-07-02T04:00:00Z", + "temp": 30 + }, { + "date": "2022-07-02T05:00:00Z", + "temp": 30 + }, { + "date": "2022-07-02T06:00:00Z", + "temp": 30 + }, { + "date": "2022-07-02T07:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T08:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T09:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T10:00:00Z", + "temp": 30 + }, { + "date": "2022-07-02T11:00:00Z", + "temp": 30 + }, { + "date": "2022-07-02T12:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T13:00:00Z", + "temp": 28 + }, { + "date": "2022-07-02T14:00:00Z", + "temp": 28 + }, { + "date": "2022-07-02T15:00:00Z", + "temp": 28 + }, { + "date": "2022-07-02T16:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T17:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T18:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T19:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T20:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T21:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T22:00:00Z", + "temp": 29 + }, { + "date": "2022-07-02T23:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T00:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T01:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T03:00:00Z", + "temp": 32 + }, { + "date": "2022-07-03T04:00:00Z", + "temp": 31 + }, { + "date": "2022-07-03T05:00:00Z", + "temp": 30 + }, { + "date": "2022-07-03T06:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T07:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T08:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T09:00:00Z", + "temp": 30 + }, { + "date": "2022-07-03T10:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T11:00:00Z", + "temp": 31 + }, { + "date": "2022-07-03T12:00:00Z", + "temp": 30 + }, { + "date": "2022-07-03T13:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T14:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T15:00:00Z", + "temp": 30 + }, { + "date": "2022-07-03T16:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T17:00:00Z", + "temp": 29 + }, { + "date": "2022-07-03T18:00:00Z", + "temp": 30 + }, { + "date": "2022-07-03T19:00:00Z", + "temp": 30 + }, { + "date": "2022-07-03T20:00:00Z", + "temp": 30 + }, { + "date": "2022-07-03T21:00:00Z", + "temp": 31 + }, { + "date": "2022-07-03T22:00:00Z", + "temp": 32 + }, { + "date": "2022-07-03T23:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T00:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T01:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T03:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T04:00:00Z", + "temp": 32 + }, { + "date": "2022-07-04T05:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T06:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T07:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T08:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T09:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T10:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T11:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T12:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T13:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T14:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T15:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T16:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T17:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T18:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T19:00:00Z", + "temp": 30 + }, { + "date": "2022-07-04T20:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T21:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T22:00:00Z", + "temp": 31 + }, { + "date": "2022-07-04T23:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T00:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T01:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T03:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T04:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T05:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T06:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T07:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T08:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T09:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T10:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T11:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T12:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T13:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T14:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T15:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T16:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T17:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T18:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T19:00:00Z", + "temp": 31 + }, { + "date": "2022-07-05T20:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T21:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T22:00:00Z", + "temp": 32 + }, { + "date": "2022-07-05T23:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T00:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T01:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T02:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T03:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T04:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T05:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T06:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T07:00:00Z", + "temp": 37 + }, { + "date": "2022-07-06T08:00:00Z", + "temp": 36 + }, { + "date": "2022-07-06T09:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T10:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T11:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T12:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T13:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T14:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T15:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T16:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T17:00:00Z", + "temp": 33 + }, { + "date": "2022-07-06T18:00:00Z", + "temp": 33 + }, { + "date": "2022-07-06T19:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T20:00:00Z", + "temp": 34 + }, { + "date": "2022-07-06T21:00:00Z", + "temp": 32 + }, { + "date": "2022-07-06T22:00:00Z", + "temp": 31 + }, { + "date": "2022-07-06T23:00:00Z", + "temp": 31 + }, { + "date": "2022-07-07T00:00:00Z", + "temp": 31 + }, { + "date": "2022-07-07T01:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T02:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T03:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T04:00:00Z", + "temp": 31 + }, { + "date": "2022-07-07T05:00:00Z", + "temp": 31 + }, { + "date": "2022-07-07T06:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T07:00:00Z", + "temp": 31 + }, { + "date": "2022-07-07T08:00:00Z", + "temp": 31 + }, { + "date": "2022-07-07T09:00:00Z", + "temp": 31 + }, { + "date": "2022-07-07T10:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T11:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T12:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T13:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T14:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T15:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T16:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T17:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T18:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T19:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T20:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T21:00:00Z", + "temp": 30 + }, { + "date": "2022-07-07T22:00:00Z", + "temp": 31 + }, { + "date": "2022-07-07T23:00:00Z", + "temp": 31 + }, { + "date": "2022-07-08T00:00:00Z", + "temp": 31 + }, { + "date": "2022-07-08T01:00:00Z", + "temp": 31 + }, { + "date": "2022-07-08T02:00:00Z", + "temp": 30 + }, { + "date": "2022-07-08T03:00:00Z", + "temp": 31 + }, { + "date": "2022-07-08T04:00:00Z", + "temp": 31 + }, { + "date": "2022-07-08T05:00:00Z", + "temp": 32 + }, { + "date": "2022-07-08T06:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T07:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T08:00:00Z", + "temp": 34 + }, { + "date": "2022-07-08T09:00:00Z", + "temp": 31 + }, { + "date": "2022-07-08T10:00:00Z", + "temp": 30 + }, { + "date": "2022-07-08T11:00:00Z", + "temp": 30 + }, { + "date": "2022-07-08T12:00:00Z", + "temp": 30 + }, { + "date": "2022-07-08T13:00:00Z", + "temp": 31 + }, { + "date": "2022-07-08T14:00:00Z", + "temp": 31 + }, { + "date": "2022-07-08T15:00:00Z", + "temp": 31 + }] + } + }, + "success": true +} diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.spec.ts b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.spec.ts index db01c53..26248f1 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.spec.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.spec.ts @@ -1,25 +1,64 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; + +import {DashboardDeviceDeleteDialogComponent} from './dashboard-device-delete-dialog.component'; +import {HttpClientModule} from '@angular/common/http'; +import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog'; +import {MatButtonModule} from '@angular/material/button'; +import {MatIconModule} from '@angular/material/icon'; +import {SharedModule} from '../../../shared/shared.module'; +import {DashboardDeviceDeleteDialogService} from './dashboard-device-delete-dialog.service'; +import {of} from 'rxjs'; -import { DashboardDeviceDeleteDialogComponent } from './dashboard-device-delete-dialog.component'; describe('DashboardDeviceDeleteDialogComponent', () => { - let component: DashboardDeviceDeleteDialogComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ DashboardDeviceDeleteDialogComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(DashboardDeviceDeleteDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); + let component: DashboardDeviceDeleteDialogComponent; + let fixture: ComponentFixture; + + const matDialogRefSpy = jasmine.createSpyObj('MatDialogRef', ['closeDialog', 'close']); + const dashboardDeviceDeleteDialogServiceSpy = jasmine.createSpyObj('DashboardDeviceDeleteDialogService', ['deleteDevice']); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientModule, + MatDialogModule, + MatButtonModule, + MatIconModule, + SharedModule, + ], + providers: [ + {provide: MatDialogRef, useValue: matDialogRefSpy}, + {provide: MAT_DIALOG_DATA, useValue: {wwn: 'test-wwn', title: 'my-test-device-title'}}, + {provide: DashboardDeviceDeleteDialogService, useValue: dashboardDeviceDeleteDialogServiceSpy} + ], + declarations: [DashboardDeviceDeleteDialogComponent] + }) + .compileComponents() + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DashboardDeviceDeleteDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should close the component if cancel is clicked', () => { + matDialogRefSpy.closeDialog.calls.reset(); + matDialogRefSpy.closeDialog() + expect(matDialogRefSpy.closeDialog).toHaveBeenCalled(); + }); + + it('should attempt to delete device if delete is clicked', () => { + dashboardDeviceDeleteDialogServiceSpy.deleteDevice.and.returnValue(of({'success': true})); + + component.onDeleteClick() + expect(dashboardDeviceDeleteDialogServiceSpy.deleteDevice).toHaveBeenCalledWith('test-wwn'); + expect(dashboardDeviceDeleteDialogServiceSpy.deleteDevice.calls.count()) + .withContext('one call') + .toBe(1); + }); }); diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.ts b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.ts index d995887..5fdd8a0 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component.ts @@ -1,7 +1,6 @@ -import { Component, OnInit, Inject } from '@angular/core'; +import {Component, Inject, OnInit} from '@angular/core'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; import {DashboardDeviceDeleteDialogService} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.service'; -import {Subject} from 'rxjs'; @Component({ selector: 'app-dashboard-device-delete-dialog', diff --git a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module.ts b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module.ts index 2605777..5d3799f 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module.ts @@ -1,44 +1,21 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { Overlay } from '@angular/cdk/overlay'; -import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatButtonModule } from '@angular/material/button'; -import { MatSelectModule } from '@angular/material/select'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { SharedModule } from 'app/shared/shared.module'; +import {NgModule} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {MatButtonModule} from '@angular/material/button'; +import {MatIconModule} from '@angular/material/icon'; +import {SharedModule} from 'app/shared/shared.module'; import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component' -import { MatButtonToggleModule} from '@angular/material/button-toggle'; -import {MatTabsModule} from '@angular/material/tabs'; -import {MatSliderModule} from '@angular/material/slider'; -import {MatSlideToggleModule} from '@angular/material/slide-toggle'; -import {MatTooltipModule} from '@angular/material/tooltip'; import {dashboardRoutes} from 'app/modules/dashboard/dashboard.routing'; -import {MatDividerModule} from '@angular/material/divider'; -import {MatMenuModule} from '@angular/material/menu'; -import {MatProgressBarModule} from '@angular/material/progress-bar'; -import {MatSortModule} from '@angular/material/sort'; -import {MatTableModule} from '@angular/material/table'; -import {NgApexchartsModule} from 'ng-apexcharts'; -import { MatDialogModule } from '@angular/material/dialog'; +import {MatDialogModule} from '@angular/material/dialog'; @NgModule({ declarations: [ DashboardDeviceDeleteDialogComponent ], - imports : [ + imports: [ RouterModule.forChild([]), RouterModule.forChild(dashboardRoutes), MatButtonModule, - MatDividerModule, - MatTooltipModule, MatIconModule, - MatMenuModule, - MatProgressBarModule, - MatSortModule, - MatTableModule, - NgApexchartsModule, SharedModule, MatDialogModule ], diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.spec.ts b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.spec.ts index dba412c..7b334bb 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.spec.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.spec.ts @@ -1,25 +1,105 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import { DashboardDeviceComponent } from './dashboard-device.component'; +import {DashboardDeviceComponent} from './dashboard-device.component'; +import {MatDialog} from '@angular/material/dialog'; +import {MatButtonModule} from '@angular/material/button'; +import {MatIconModule} from '@angular/material/icon'; +import {SharedModule} from 'app/shared/shared.module'; +import {MatMenuModule} from '@angular/material/menu'; +import {TREO_APP_CONFIG} from '@treo/services/config/config.constants'; +import {DeviceSummaryModel} from 'app/core/models/device-summary-model'; +import * as moment from 'moment'; describe('DashboardDeviceComponent', () => { - let component: DashboardDeviceComponent; - let fixture: ComponentFixture; + let component: DashboardDeviceComponent; + let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ DashboardDeviceComponent ] + const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']); + // const configServiceSpy = jasmine.createSpyObj('TreoConfigService', ['config$']); + + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + MatButtonModule, + MatIconModule, + MatMenuModule, + SharedModule, + ], + providers: [ + {provide: MatDialog, useValue: matDialogSpy}, + {provide: TREO_APP_CONFIG, useValue: {dashboardDisplay: 'name'}} + ], + declarations: [DashboardDeviceComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + // configServiceSpy.config$.and.returnValue(of({'success': true})); + fixture = TestBed.createComponent(DashboardDeviceComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('#classDeviceLastUpdatedOn()', () => { + + it('if non-zero device status, should be red', () => { + // component.deviceSummary = summary.data.summary['0x5000c500673e6b5f'] as DeviceSummaryModel + expect(component.classDeviceLastUpdatedOn({ + device: { + device_status: 2 + } + } as DeviceSummaryModel)).toBe('text-red') + }); + + it('if non-zero device status, should be red', () => { + // component.deviceSummary = summary.data.summary['0x5000c500673e6b5f'] as DeviceSummaryModel + expect(component.classDeviceLastUpdatedOn({ + device: { + device_status: 2 + } + } as DeviceSummaryModel)).toBe('text-red') + }); + + it('if healthy device status and updated in the last two weeks, should be green', () => { + // component.deviceSummary = summary.data.summary['0x5000c500673e6b5f'] as DeviceSummaryModel + expect(component.classDeviceLastUpdatedOn({ + device: { + device_status: 0 + }, + smart: { + collector_date: moment().subtract(13, 'days').toISOString() + } + } as DeviceSummaryModel)).toBe('text-green') + }); + + it('if healthy device status and updated more than two weeks ago, but less than 1 month, should be yellow', () => { + // component.deviceSummary = summary.data.summary['0x5000c500673e6b5f'] as DeviceSummaryModel + expect(component.classDeviceLastUpdatedOn({ + device: { + device_status: 0 + }, + smart: { + collector_date: moment().subtract(3, 'weeks').toISOString() + } + } as DeviceSummaryModel)).toBe('text-yellow') + }); + + it('if healthy device status and updated more 1 month ago, should be red', () => { + // component.deviceSummary = summary.data.summary['0x5000c500673e6b5f'] as DeviceSummaryModel + expect(component.classDeviceLastUpdatedOn({ + device: { + device_status: 0 + }, + smart: { + collector_date: moment().subtract(5, 'weeks').toISOString() + } + } as DeviceSummaryModel)).toBe('text-red') + }); }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(DashboardDeviceComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); + }); diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts index d2c9859..4fb7d7a 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.component.ts @@ -1,18 +1,19 @@ -import { Component, Input, Output, OnInit, EventEmitter} from '@angular/core'; +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; import * as moment from 'moment'; import {takeUntil} from 'rxjs/operators'; import {AppConfig} from 'app/core/config/app.config'; import {TreoConfigService} from '@treo/services/config'; import {Subject} from 'rxjs'; -import humanizeDuration from 'humanize-duration' +import humanizeDuration from 'humanize-duration' import {MatDialog} from '@angular/material/dialog'; import {DashboardDeviceDeleteDialogComponent} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.component'; import {DeviceTitlePipe} from 'app/shared/device-title.pipe'; +import {DeviceSummaryModel} from 'app/core/models/device-summary-model'; @Component({ - selector: 'app-dashboard-device', - templateUrl: './dashboard-device.component.html', - styleUrls: ['./dashboard-device.component.scss'] + selector: 'app-dashboard-device', + templateUrl: './dashboard-device.component.html', + styleUrls: ['./dashboard-device.component.scss'] }) export class DashboardDeviceComponent implements OnInit { @@ -23,7 +24,8 @@ export class DashboardDeviceComponent implements OnInit { // Set the private defaults this._unsubscribeAll = new Subject(); } - @Input() deviceSummary: any; + + @Input() deviceSummary: DeviceSummaryModel; @Input() deviceWWN: string; @Output() deviceDeleted = new EventEmitter(); @@ -47,28 +49,27 @@ export class DashboardDeviceComponent implements OnInit { // @ Public methods // ----------------------------------------------------------------------------------------------------- - classDeviceLastUpdatedOn(deviceSummary): string { + classDeviceLastUpdatedOn(deviceSummary: DeviceSummaryModel): string { if (deviceSummary.device.device_status !== 0) { return 'text-red' // if the device has failed, always highlight in red - } else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){ - if(moment().subtract(14, 'd').isBefore(deviceSummary.smart.collector_date)){ + } else if (deviceSummary.device.device_status === 0 && deviceSummary.smart) { + if (moment().subtract(14, 'days').isBefore(deviceSummary.smart.collector_date)) { // this device was updated in the last 2 weeks. return 'text-green' - } else if(moment().subtract(1, 'm').isBefore(deviceSummary.smart.collector_date)){ + } else if (moment().subtract(1, 'months').isBefore(deviceSummary.smart.collector_date)) { // this device was updated in the last month return 'text-yellow' - } else{ + } else { // last updated more than a month ago. return 'text-red' } - } else { return '' } } - deviceStatusString(deviceStatus): string { - if(deviceStatus === 0){ + deviceStatusString(deviceStatus: number): string { + if (deviceStatus === 0) { return 'passed' } else { return 'failed' @@ -76,16 +77,18 @@ export class DashboardDeviceComponent implements OnInit { } - openDeleteDialog(): void { const dialogRef = this.dialog.open(DashboardDeviceDeleteDialogComponent, { // width: '250px', - data: {wwn: this.deviceWWN, title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboardDisplay)} + data: { + wwn: this.deviceWWN, + title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboardDisplay) + } }); dialogRef.afterClosed().subscribe(result => { console.log('The dialog was closed', result); - if(result.success){ + if (result.success) { this.deviceDeleted.emit(this.deviceWWN) } }); diff --git a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.module.ts b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.module.ts index e338330..924c145 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.module.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-device/dashboard-device.module.ts @@ -1,53 +1,30 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { Overlay } from '@angular/cdk/overlay'; -import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatButtonModule } from '@angular/material/button'; -import { MatSelectModule } from '@angular/material/select'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { SharedModule } from 'app/shared/shared.module'; +import {NgModule} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {MatButtonModule} from '@angular/material/button'; +import {MatIconModule} from '@angular/material/icon'; +import {SharedModule} from 'app/shared/shared.module'; import {DashboardDeviceComponent} from 'app/layout/common/dashboard-device/dashboard-device.component' -import { MatDialogModule } from '@angular/material/dialog'; -import { MatButtonToggleModule} from '@angular/material/button-toggle'; -import {MatTabsModule} from '@angular/material/tabs'; -import {MatSliderModule} from '@angular/material/slider'; -import {MatSlideToggleModule} from '@angular/material/slide-toggle'; -import {MatTooltipModule} from '@angular/material/tooltip'; import {dashboardRoutes} from '../../../modules/dashboard/dashboard.routing'; -import {MatDividerModule} from '@angular/material/divider'; import {MatMenuModule} from '@angular/material/menu'; -import {MatProgressBarModule} from '@angular/material/progress-bar'; -import {MatSortModule} from '@angular/material/sort'; -import {MatTableModule} from '@angular/material/table'; -import {NgApexchartsModule} from 'ng-apexcharts'; import {DashboardDeviceDeleteDialogModule} from 'app/layout/common/dashboard-device-delete-dialog/dashboard-device-delete-dialog.module'; @NgModule({ declarations: [ DashboardDeviceComponent ], - imports : [ + imports: [ RouterModule.forChild([]), RouterModule.forChild(dashboardRoutes), MatButtonModule, - MatDividerModule, - MatTooltipModule, MatIconModule, MatMenuModule, - MatProgressBarModule, - MatSortModule, - MatTableModule, - NgApexchartsModule, SharedModule, DashboardDeviceDeleteDialogModule ], - exports : [ + exports: [ DashboardDeviceComponent, ], - providers : [] + providers: [] }) -export class DashboardDeviceModule -{ +export class DashboardDeviceModule { } diff --git a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.spec.ts b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.spec.ts deleted file mode 100644 index 95a052f..0000000 --- a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DashboardSettingsComponent } from './dashboard-settings.component'; - -describe('DashboardSettingsComponent', () => { - let component: DashboardSettingsComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ DashboardSettingsComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(DashboardSettingsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts index 893aadf..70a0978 100644 --- a/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts +++ b/webapp/frontend/src/app/layout/common/dashboard-settings/dashboard-settings.component.ts @@ -1,13 +1,13 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import {AppConfig} from 'app/core/config/app.config'; -import { TreoConfigService } from '@treo/services/config'; +import {TreoConfigService} from '@treo/services/config'; import {Subject} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; @Component({ - selector: 'app-dashboard-settings', - templateUrl: './dashboard-settings.component.html', - styleUrls: ['./dashboard-settings.component.scss'] + selector: 'app-dashboard-settings', + templateUrl: './dashboard-settings.component.html', + styleUrls: ['./dashboard-settings.component.scss'] }) export class DashboardSettingsComponent implements OnInit { @@ -26,25 +26,23 @@ export class DashboardSettingsComponent implements OnInit { this._unsubscribeAll = new Subject(); } - ngOnInit(): void { - // Subscribe to config changes - this._configService.config$ - .pipe(takeUntil(this._unsubscribeAll)) - .subscribe((config: AppConfig) => { + ngOnInit(): void { + // Subscribe to config changes + this._configService.config$ + .pipe(takeUntil(this._unsubscribeAll)) + .subscribe((config: AppConfig) => { - // Store the config - this.dashboardDisplay = config.dashboardDisplay; - this.dashboardSort = config.dashboardSort; - this.temperatureUnit = config.temperatureUnit; - this.theme = config.theme; + // Store the config + this.dashboardDisplay = config.dashboardDisplay; + this.dashboardSort = config.dashboardSort; + this.temperatureUnit = config.temperatureUnit; + this.theme = config.theme; - }); - - } - - saveSettings(): void { + }); + } + saveSettings(): void { const newSettings = { dashboardDisplay: this.dashboardDisplay, dashboardSort: this.dashboardSort, @@ -53,7 +51,7 @@ export class DashboardSettingsComponent implements OnInit { } this._configService.config = newSettings console.log(`Saved Settings: ${JSON.stringify(newSettings)}`) - } + } formatLabel(value: number): number { return value; diff --git a/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.module.ts b/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.module.ts index b8c05df..a85e0ca 100644 --- a/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.module.ts +++ b/webapp/frontend/src/app/layout/common/detail-settings/detail-settings.module.ts @@ -1,16 +1,15 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { Overlay } from '@angular/cdk/overlay'; -import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatButtonModule } from '@angular/material/button'; -import { MatSelectModule } from '@angular/material/select'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { SharedModule } from 'app/shared/shared.module'; +import {NgModule} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; +import {MatButtonModule} from '@angular/material/button'; +import {MatSelectModule} from '@angular/material/select'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatIconModule} from '@angular/material/icon'; +import {MatInputModule} from '@angular/material/input'; +import {SharedModule} from 'app/shared/shared.module'; import {DetailSettingsComponent} from 'app/layout/common/detail-settings/detail-settings.component' -import { MatDialogModule } from '@angular/material/dialog'; -import { MatButtonToggleModule} from '@angular/material/button-toggle'; +import {MatDialogModule} from '@angular/material/dialog'; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; import {MatTabsModule} from '@angular/material/tabs'; import {MatSliderModule} from '@angular/material/slider'; import {MatSlideToggleModule} from '@angular/material/slide-toggle'; @@ -20,7 +19,7 @@ import {MatTooltipModule} from '@angular/material/tooltip'; declarations: [ DetailSettingsComponent ], - imports : [ + imports: [ RouterModule.forChild([]), MatAutocompleteModule, MatDialogModule, @@ -36,11 +35,10 @@ import {MatTooltipModule} from '@angular/material/tooltip'; MatSlideToggleModule, SharedModule ], - exports : [ + exports: [ DetailSettingsComponent ], - providers : [] + providers: [] }) -export class DetailSettingsModule -{ +export class DetailSettingsModule { } diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html index cbb19a4..d370ab5 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.html +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.html @@ -1,5 +1,5 @@ -
+
diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts index eb5a2cc..7352e98 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.component.ts @@ -1,17 +1,24 @@ -import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; -import { MatSort } from '@angular/material/sort'; -import { MatTableDataSource } from '@angular/material/table'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + OnDestroy, + OnInit, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import {Subject} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; import {ApexOptions, ChartComponent} from 'ng-apexcharts'; -import { DashboardService } from 'app/modules/dashboard/dashboard.service'; +import {DashboardService} from 'app/modules/dashboard/dashboard.service'; import {MatDialog} from '@angular/material/dialog'; -import { DashboardSettingsComponent } from 'app/layout/common/dashboard-settings/dashboard-settings.component'; +import {DashboardSettingsComponent} from 'app/layout/common/dashboard-settings/dashboard-settings.component'; import {AppConfig} from 'app/core/config/app.config'; import {TreoConfigService} from '@treo/services/config'; import {Router} from '@angular/router'; import {TemperaturePipe} from 'app/shared/temperature.pipe'; import {DeviceTitlePipe} from 'app/shared/device-title.pipe'; +import {DeviceSummaryModel} from 'app/core/models/device-summary-model'; @Component({ selector : 'example', @@ -22,7 +29,7 @@ import {DeviceTitlePipe} from 'app/shared/device-title.pipe'; }) export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy { - data: any; + summaryData: { [key: string]: DeviceSummaryModel }; hostGroups: { [hostId: string]: string[] } = {} temperatureOptions: ApexOptions; tempDurationKey = 'forever' @@ -35,10 +42,13 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy /** * Constructor * - * @param {SmartService} _smartService + * @param {DashboardService} _dashboardService + * @param {TreoConfigService} _configService + * @param {MatDialog} dialog + * @param {Router} router */ constructor( - private _smartService: DashboardService, + private _dashboardService: DashboardService, private _configService: TreoConfigService, public dialog: MatDialog, private router: Router, @@ -81,16 +91,16 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy }); // Get the data - this._smartService.data$ + this._dashboardService.data$ .pipe(takeUntil(this._unsubscribeAll)) .subscribe((data) => { // Store the data - this.data = data; + this.summaryData = data; // generate group data. - for(const wwn in this.data.data.summary){ - const hostid = this.data.data.summary[wwn].device.host_id + for (const wwn in this.summaryData) { + const hostid = this.summaryData[wwn].device.host_id const hostDeviceList = this.hostGroups[hostid] || [] hostDeviceList.push(wwn) this.hostGroups[hostid] = hostDeviceList @@ -132,11 +142,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy private _deviceDataTemperatureSeries(): any[] { const deviceTemperatureSeries = [] - console.log('DEVICE DATA SUMMARY', this.data) + console.log('DEVICE DATA SUMMARY', this.summaryData) - for(const wwn in this.data.data.summary){ - const deviceSummary = this.data.data.summary[wwn] - if (!deviceSummary.temp_history){ + for (const wwn in this.summaryData) { + const deviceSummary = this.summaryData[wwn] + if (!deviceSummary.temp_history) { continue } @@ -206,7 +216,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy } } }, - xaxis : { + xaxis: { type: 'datetime' } }; @@ -216,11 +226,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy // @ Public methods // ----------------------------------------------------------------------------------------------------- - deviceSummariesForHostGroup(hostGroupWWNs: string[]): any[] { - const deviceSummaries = [] - for(const wwn of hostGroupWWNs){ - if(this.data.data.summary[wwn]){ - deviceSummaries.push(this.data.data.summary[wwn]) + deviceSummariesForHostGroup(hostGroupWWNs: string[]): DeviceSummaryModel[] { + const deviceSummaries: DeviceSummaryModel[] = [] + for (const wwn of hostGroupWWNs) { + if (this.summaryData[wwn]) { + deviceSummaries.push(this.summaryData[wwn]) } } return deviceSummaries @@ -235,7 +245,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy } onDeviceDeleted(wwn: string): void { - delete this.data.data.summary[wwn] // remove the device from the summary list. + delete this.summaryData[wwn] // remove the device from the summary list. } /* @@ -246,16 +256,16 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy DURATION_KEY_FOREVER = "forever" */ - changeSummaryTempDuration(durationKey: string){ + changeSummaryTempDuration(durationKey: string): void { this.tempDurationKey = durationKey - this._smartService.getSummaryTempData(durationKey) - .subscribe((data) => { + this._dashboardService.getSummaryTempData(durationKey) + .subscribe((tempHistoryData) => { // given a list of device temp history, override the data in the "summary" object. - for(const wwn in this.data.data.summary) { + for (const wwn in this.summaryData) { // console.log(`Updating ${wwn}, length: ${this.data.data.summary[wwn].temp_history.length}`) - this.data.data.summary[wwn].temp_history = data.data.temp_history[wwn] || [] + this.summaryData[wwn].temp_history = tempHistoryData[wwn] || [] } // Prepare the chart series data diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts index eb29b48..a235061 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.resolvers.ts @@ -1,13 +1,13 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; -import { Observable } from 'rxjs'; -import { DashboardService } from 'app/modules/dashboard/dashboard.service'; +import {Injectable} from '@angular/core'; +import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router'; +import {Observable} from 'rxjs'; +import {DashboardService} from 'app/modules/dashboard/dashboard.service'; +import {DeviceSummaryModel} from 'app/core/models/device-summary-model'; @Injectable({ providedIn: 'root' }) -export class DashboardResolver implements Resolve -{ +export class DashboardResolver implements Resolve { /** * Constructor * @@ -29,8 +29,7 @@ export class DashboardResolver implements Resolve * @param route * @param state */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable - { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<{ [p: string]: DeviceSummaryModel }> { return this._dashboardService.getSummaryData(); } } diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.service.spec.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.service.spec.ts new file mode 100644 index 0000000..75a7679 --- /dev/null +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.service.spec.ts @@ -0,0 +1,44 @@ +import {HttpClient} from '@angular/common/http'; +import {DashboardService} from './dashboard.service'; +import {of} from 'rxjs'; +import {summary} from 'app/data/mock/summary/data' +import {temp_history} from 'app/data/mock/summary/temp_history' +import {DeviceSummaryModel} from 'app/core/models/device-summary-model'; +import {SmartTemperatureModel} from 'app/core/models/measurements/smart-temperature-model'; + +describe('DashboardService', () => { + let service: DashboardService; + let httpClientSpy: jasmine.SpyObj; + + beforeEach(() => { + httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']); + service = new DashboardService(httpClientSpy); + }); + + it('should unwrap and return getSummaryData() (HttpClient called once)', (done: DoneFn) => { + httpClientSpy.get.and.returnValue(of(summary)); + + service.getSummaryData().subscribe(value => { + expect(value).toBe(summary.data.summary as { [key: string]: DeviceSummaryModel }); + done(); + }); + expect(httpClientSpy.get.calls.count()) + .withContext('one call') + .toBe(1); + }); + + it('should unwrap and return getSummaryTempData() (HttpClient called once)', (done: DoneFn) => { + // const expectedHeroes: any[] = + // [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]; + + httpClientSpy.get.and.returnValue(of(temp_history)); + + service.getSummaryTempData('weekly').subscribe(value => { + expect(value).toBe(temp_history.data.temp_history as { [key: string]: SmartTemperatureModel[] }); + done(); + }); + expect(httpClientSpy.get.calls.count()) + .withContext('one call') + .toBe(1); + }); +}); diff --git a/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts b/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts index 185da9d..35764d0 100644 --- a/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts +++ b/webapp/frontend/src/app/modules/dashboard/dashboard.service.ts @@ -1,16 +1,19 @@ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { getBasePath } from 'app/app.routing'; +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {BehaviorSubject, Observable} from 'rxjs'; +import {map, tap} from 'rxjs/operators'; +import {getBasePath} from 'app/app.routing'; +import {DeviceSummaryResponseWrapper} from 'app/core/models/device-summary-response-wrapper'; +import {DeviceSummaryModel} from 'app/core/models/device-summary-model'; +import {SmartTemperatureModel} from 'app/core/models/measurements/smart-temperature-model'; +import {DeviceSummaryTempResponseWrapper} from 'app/core/models/device-summary-temp-response-wrapper'; @Injectable({ providedIn: 'root' }) -export class DashboardService -{ +export class DashboardService { // Observables - private _data: BehaviorSubject; + private _data: BehaviorSubject<{ [p: string]: DeviceSummaryModel }>; /** * Constructor @@ -32,8 +35,7 @@ export class DashboardService /** * Getter for data */ - get data$(): Observable - { + get data$(): Observable<{ [p: string]: DeviceSummaryModel }> { return this._data.asObservable(); } @@ -44,22 +46,28 @@ export class DashboardService /** * Get data */ - getSummaryData(): Observable - { + getSummaryData(): Observable<{ [key: string]: DeviceSummaryModel }> { return this._httpClient.get(getBasePath() + '/api/summary').pipe( - tap((response: any) => { + map((response: DeviceSummaryResponseWrapper) => { + // console.log("FILTERING=----", response.data.summary) + return response.data.summary + }), + tap((response: { [key: string]: DeviceSummaryModel }) => { this._data.next(response); }) ); } - getSummaryTempData(durationKey: string): Observable - { + getSummaryTempData(durationKey: string): Observable<{ [key: string]: SmartTemperatureModel[] }> { const params = {} - if(durationKey){ + if (durationKey) { params['duration_key'] = durationKey } - return this._httpClient.get(getBasePath() + '/api/summary/temp', {params: params}); + return this._httpClient.get(getBasePath() + '/api/summary/temp', {params: params}).pipe( + map((response: DeviceSummaryTempResponseWrapper) => { + return response.data.temp_history + }) + ); } } diff --git a/webapp/frontend/src/app/modules/detail/detail.component.spec.ts b/webapp/frontend/src/app/modules/detail/detail.component.spec.ts deleted file mode 100644 index 6f956ee..0000000 --- a/webapp/frontend/src/app/modules/detail/detail.component.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { HttpClientModule } from '@angular/common/http'; -import { DetailComponent } from './detail.component'; - -import {TreoConfigService} from '@treo/services/config'; -import { TREO_APP_CONFIG } from '@treo/services/config/config.constants'; -const TREO_APP_CONFIG_PROVIDER = [ { provide: TREO_APP_CONFIG, useValue: TreoConfigService } ]; -import { MatDialogModule } from '@angular/material/dialog'; -import {DeviceTitlePipe} from 'app/shared/device-title.pipe'; - - -describe('DetailComponent', () => { - let component: DetailComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [ - HttpClientModule, - MatDialogModule - - ], - declarations: [ DetailComponent, DeviceTitlePipe ], - providers: [ TREO_APP_CONFIG_PROVIDER ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(DetailComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/webapp/frontend/src/app/modules/detail/detail.component.ts b/webapp/frontend/src/app/modules/detail/detail.component.ts index f37910a..0e29652 100644 --- a/webapp/frontend/src/app/modules/detail/detail.component.ts +++ b/webapp/frontend/src/app/modules/detail/detail.component.ts @@ -1,18 +1,21 @@ -import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import humanizeDuration from 'humanize-duration'; +import {AfterViewInit, Component, Inject, LOCALE_ID, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {ApexOptions} from 'ng-apexcharts'; -import {MatTableDataSource} from '@angular/material/table'; -import {MatSort} from '@angular/material/sort'; -import {Subject} from 'rxjs'; +import {AppConfig} from 'app/core/config/app.config'; import {DetailService} from './detail.service'; -import {takeUntil} from 'rxjs/operators'; import {DetailSettingsComponent} from 'app/layout/common/detail-settings/detail-settings.component'; import {MatDialog} from '@angular/material/dialog'; -import humanizeDuration from 'humanize-duration'; +import {MatSort} from '@angular/material/sort'; +import {MatTableDataSource} from '@angular/material/table'; +import {Subject} from 'rxjs'; import {TreoConfigService} from '@treo/services/config'; -import {AppConfig} from 'app/core/config/app.config'; import {animate, state, style, transition, trigger} from '@angular/animations'; import {formatDate} from '@angular/common'; -import { LOCALE_ID, Inject } from '@angular/core'; +import {takeUntil} from 'rxjs/operators'; +import {DeviceModel} from 'app/core/models/device-model'; +import {SmartModel} from 'app/core/models/measurements/smart-model'; +import {SmartAttributeModel} from 'app/core/models/measurements/smart-attribute-model'; +import {AttributeMetadataModel} from 'app/core/models/thresholds/attribute-metadata-model'; // from Constants.go - these must match const AttributeStatusPassed = 0 @@ -22,9 +25,9 @@ const AttributeStatusFailedScrutiny = 4 @Component({ - selector: 'detail', - templateUrl: './detail.component.html', - styleUrls: ['./detail.component.scss'], + selector: 'detail', + templateUrl: './detail.component.html', + styleUrls: ['./detail.component.scss'], animations: [ trigger('detailExpand', [ state('collapsed', style({height: '0px', minHeight: '0'})), @@ -40,22 +43,23 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { * Constructor * * @param {DetailService} _detailService + * @param {MatDialog} dialog + * @param {TreoConfigService} _configService + * @param {string} locale */ constructor( private _detailService: DetailService, public dialog: MatDialog, private _configService: TreoConfigService, @Inject(LOCALE_ID) public locale: string - - ) - { + ) { // Set the private defaults this._unsubscribeAll = new Subject(); // Set the defaults this.smartAttributeDataSource = new MatTableDataSource(); // this.recentTransactionsTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh']; - this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh','ideal', 'failure', 'history']; + this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh', 'ideal', 'failure', 'history']; this.systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; @@ -65,14 +69,15 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { onlyCritical = true; // data: any; - expandedAttribute: any | null; + expandedAttribute: SmartAttributeModel | null; - metadata: any; - device: any; - smart_results: any[]; + metadata: { [p: string]: AttributeMetadataModel } | { [p: number]: AttributeMetadataModel }; + device: DeviceModel; + // tslint:disable-next-line:variable-name + smart_results: SmartModel[]; commonSparklineOptions: Partial; - smartAttributeDataSource: MatTableDataSource; + smartAttributeDataSource: MatTableDataSource; smartAttributeTableColumns: string[]; @ViewChild('smartAttributeTable', {read: MatSort}) @@ -91,8 +96,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { /** * On init */ - ngOnInit(): void - { + ngOnInit(): void { // Subscribe to config changes this._configService.config$ .pipe(takeUntil(this._unsubscribeAll)) @@ -104,13 +108,13 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { // Get the data this._detailService.data$ .pipe(takeUntil(this._unsubscribeAll)) - .subscribe((data) => { + .subscribe((respWrapper) => { // Store the data // this.data = data; - this.device = data.data.device; - this.smart_results = data.data.smart_results - this.metadata = data.metadata; + this.device = respWrapper.data.device; + this.smart_results = respWrapper.data.smart_results + this.metadata = respWrapper.metadata; // Store the table data @@ -124,8 +128,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { /** * After view init */ - ngAfterViewInit(): void - { + ngAfterViewInit(): void { // Make the data source sortable this.smartAttributeDataSource.sort = this.smartAttributeTableMatSort; } @@ -133,8 +136,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { /** * On destroy */ - ngOnDestroy(): void - { + ngOnDestroy(): void { // Unsubscribe from all subscriptions this._unsubscribeAll.next(); this._unsubscribeAll.complete(); @@ -147,22 +149,23 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { getAttributeStatusName(attributeStatus: number): string { // tslint:disable:no-bitwise - if(attributeStatus === AttributeStatusPassed){ + if (attributeStatus === AttributeStatusPassed) { return 'passed' - } else if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0 || (attributeStatus & AttributeStatusFailedSmart) !== 0 ){ + } else if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0 || (attributeStatus & AttributeStatusFailedSmart) !== 0) { return 'failed' - } else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0){ + } else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0) { return 'warn' } return '' // tslint:enable:no-bitwise } + getAttributeScrutinyStatusName(attributeStatus: number): string { // tslint:disable:no-bitwise - if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0){ + if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0) { return 'failed' - } else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0){ + } else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0) { return 'warn' } else { return 'passed' @@ -172,7 +175,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { getAttributeSmartStatusName(attributeStatus: number): string { // tslint:disable:no-bitwise - if ((attributeStatus & AttributeStatusFailedSmart) !== 0){ + if ((attributeStatus & AttributeStatusFailedSmart) !== 0) { return 'failed' } else { return 'passed' @@ -181,138 +184,140 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { } - getAttributeName(attribute_data): string { - const attribute_metadata = this.metadata[attribute_data.attribute_id] - if(!attribute_metadata){ + getAttributeName(attributeData: SmartAttributeModel): string { + const attributeMetadata = this.metadata[attributeData.attribute_id] + if (!attributeMetadata) { return 'Unknown Attribute Name' } else { - return attribute_metadata.display_name + return attributeMetadata.display_name } } - getAttributeDescription(attribute_data){ - const attribute_metadata = this.metadata[attribute_data.attribute_id] - if(!attribute_metadata){ + + getAttributeDescription(attributeData: SmartAttributeModel): string { + const attributeMetadata = this.metadata[attributeData.attribute_id] + if (!attributeMetadata) { return 'Unknown' } else { - return attribute_metadata.description + return attributeMetadata.description } - return } - getAttributeValue(attribute_data){ - if(this.isAta()) { - const attribute_metadata = this.metadata[attribute_data.attribute_id] - if(!attribute_metadata){ - return attribute_data.value - } else if (attribute_metadata.display_type == 'raw') { - return attribute_data.raw_value - } else if (attribute_metadata.display_type == 'transformed' && attribute_data.transformed_value) { - return attribute_data.transformed_value + getAttributeValue(attributeData: SmartAttributeModel): number { + if (this.isAta()) { + const attributeMetadata = this.metadata[attributeData.attribute_id] + if (!attributeMetadata) { + return attributeData.value + } else if (attributeMetadata.display_type === 'raw') { + return attributeData.raw_value + } else if (attributeMetadata.display_type === 'transformed' && attributeData.transformed_value) { + return attributeData.transformed_value } else { - return attribute_data.value + return attributeData.value } - } - else{ - return attribute_data.value + } else { + return attributeData.value } } - getAttributeValueType(attribute_data){ - if(this.isAta()) { - const attribute_metadata = this.metadata[attribute_data.attribute_id] - if(!attribute_metadata){ + getAttributeValueType(attributeData: SmartAttributeModel): string { + if (this.isAta()) { + const attributeMetadata = this.metadata[attributeData.attribute_id] + if (!attributeMetadata) { return '' } else { - return attribute_metadata.display_type + return attributeMetadata.display_type } } else { return '' } } - getAttributeIdeal(attribute_data){ - if(this.isAta()){ - return this.metadata[attribute_data.attribute_id]?.display_type == 'raw' ? this.metadata[attribute_data.attribute_id]?.ideal : '' + getAttributeIdeal(attributeData: SmartAttributeModel): string { + if (this.isAta()) { + return this.metadata[attributeData.attribute_id]?.display_type === 'raw' ? this.metadata[attributeData.attribute_id]?.ideal : '' } else { - return this.metadata[attribute_data.attribute_id]?.ideal + return this.metadata[attributeData.attribute_id]?.ideal } } - getAttributeWorst(attribute_data){ - const attribute_metadata = this.metadata[attribute_data.attribute_id] - if(!attribute_metadata){ - return attribute_data.worst + getAttributeWorst(attributeData: SmartAttributeModel): number | string { + const attributeMetadata = this.metadata[attributeData.attribute_id] + if (!attributeMetadata) { + return attributeData.worst } else { - return attribute_metadata?.display_type == 'normalized' ? attribute_data.worst : '' + return attributeMetadata?.display_type === 'normalized' ? attributeData.worst : '' } } - getAttributeThreshold(attribute_data){ - if(this.isAta()){ - const attribute_metadata = this.metadata[attribute_data.attribute_id] - if(!attribute_metadata || attribute_metadata.display_type == 'normalized'){ - return attribute_data.thresh + getAttributeThreshold(attributeData: SmartAttributeModel): number | string { + if (this.isAta()) { + const attributeMetadata = this.metadata[attributeData.attribute_id] + if (!attributeMetadata || attributeMetadata.display_type === 'normalized') { + return attributeData.thresh } else { // if(this.data.metadata[attribute_data.attribute_id].observed_thresholds){ // // } else { // } // return '' - return attribute_data.thresh + return attributeData.thresh } } else { - return (attribute_data.thresh == -1 ? '' : attribute_data.thresh ) + return (attributeData.thresh === -1 ? '' : attributeData.thresh) } } - getAttributeCritical(attribute_data){ - return this.metadata[attribute_data.attribute_id]?.critical + getAttributeCritical(attributeData: SmartAttributeModel): boolean { + return this.metadata[attributeData.attribute_id]?.critical } - getHiddenAttributes(){ - if (!this.smart_results || this.smart_results.length == 0) { + + getHiddenAttributes(): number { + if (!this.smart_results || this.smart_results.length === 0) { return 0 } - let attributes_length = 0 + let attributesLength = 0 const attributes = this.smart_results[0]?.attrs if (attributes) { - attributes_length = Object.keys(attributes).length + attributesLength = Object.keys(attributes).length } - return attributes_length - this.smartAttributeDataSource.data.length + return attributesLength - this.smartAttributeDataSource.data.length } isAta(): boolean { - return this.device.device_protocol == 'ATA' + return this.device.device_protocol === 'ATA' } + isScsi(): boolean { - return this.device.device_protocol == 'SCSI' + return this.device.device_protocol === 'SCSI' } + isNvme(): boolean { - return this.device.device_protocol == 'NVMe' + return this.device.device_protocol === 'NVMe' } - private _generateSmartAttributeTableDataSource(smart_results){ - const smartAttributeDataSource = []; + private _generateSmartAttributeTableDataSource(smartResults: SmartModel[]): SmartAttributeModel[] { + const smartAttributeDataSource: SmartAttributeModel[] = []; - if(smart_results.length == 0){ + if (smartResults.length === 0) { return smartAttributeDataSource } - const latest_smart_result = smart_results[0]; - let attributes = {} - if(this.isScsi()) { + const latestSmartResult = smartResults[0]; + let attributes: { [p: string]: SmartAttributeModel } = {} + if (this.isScsi()) { this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'history']; - attributes = latest_smart_result.attrs - } else if(this.isNvme()){ + attributes = latestSmartResult.attrs + } else if (this.isNvme()) { this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'ideal', 'history']; - attributes = latest_smart_result.attrs + attributes = latestSmartResult.attrs } else { // ATA - attributes = latest_smart_result.attrs - this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'thresh','ideal', 'failure', 'history']; + attributes = latestSmartResult.attrs + this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'thresh', 'ideal', 'failure', 'history']; } - for(const attrId in attributes){ + for (const attrId in attributes) { const attr = attributes[attrId] // chart history data @@ -320,18 +325,18 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { const attrHistory = [] - for (const smart_result of smart_results){ + for (const smartResult of smartResults) { // attrHistory.push(this.getAttributeValue(smart_result.attrs[attrId])) const chartDatapoint = { - x: formatDate(smart_result.date, 'MMMM dd, yyyy - HH:mm', this.locale), - y: this.getAttributeValue(smart_result.attrs[attrId]) + x: formatDate(smartResult.date, 'MMMM dd, yyyy - HH:mm', this.locale), + y: this.getAttributeValue(smartResult.attrs[attrId]) } - const attributeStatusName = this.getAttributeStatusName(smart_result.attrs[attrId].status) - if(attributeStatusName === 'failed') { + const attributeStatusName = this.getAttributeStatusName(smartResult.attrs[attrId].status) + if (attributeStatusName === 'failed') { chartDatapoint['strokeColor'] = '#F05252' chartDatapoint['fillColor'] = '#F05252' - } else if (attributeStatusName === 'warn'){ + } else if (attributeStatusName === 'warn') { chartDatapoint['strokeColor'] = '#C27803' chartDatapoint['fillColor'] = '#C27803' } @@ -350,7 +355,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { } // determine when to include the attributes in table. - if(!this.onlyCritical || this.onlyCritical && this.metadata[attr.attribute_id]?.critical || attr.value < attr.thresh){ + if (!this.onlyCritical || this.onlyCritical && this.metadata[attr.attribute_id]?.critical || attr.value < attr.thresh) { smartAttributeDataSource.push(attr) } } @@ -362,8 +367,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { * * @private */ - private _prepareChartData(): void - { + private _prepareChartData(): void { // Account balance this.commonSparklineOptions = { @@ -392,7 +396,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { }, y: { title: { - formatter: function(seriesName) { + formatter: (seriesName) => { return ''; } } @@ -410,27 +414,28 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { }; } - private determineTheme(config:AppConfig): string { + private determineTheme(config: AppConfig): string { if (config.theme === 'system') { return this.systemPrefersDark ? 'dark' : 'light' } else { return config.theme } } + // ----------------------------------------------------------------------------------------------------- // @ Public methods // ----------------------------------------------------------------------------------------------------- - toHex(decimalNumb){ + toHex(decimalNumb: number | string): string { return '0x' + Number(decimalNumb).toString(16).padStart(2, '0').toUpperCase() } - toggleOnlyCritical(){ + + toggleOnlyCritical(): void { this.onlyCritical = !this.onlyCritical this.smartAttributeDataSource.data = this._generateSmartAttributeTableDataSource(this.smart_results); - } - openDialog() { + openDialog(): void { const dialogRef = this.dialog.open(DetailSettingsComponent); dialogRef.afterClosed().subscribe(result => { @@ -444,8 +449,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy { * @param index * @param item */ - trackByFn(index: number, item: any): any - { + trackByFn(index: number, item: any): any { return index; // return item.id || index; } diff --git a/webapp/frontend/src/app/modules/detail/detail.resolvers.ts b/webapp/frontend/src/app/modules/detail/detail.resolvers.ts index b416a3d..221cad1 100644 --- a/webapp/frontend/src/app/modules/detail/detail.resolvers.ts +++ b/webapp/frontend/src/app/modules/detail/detail.resolvers.ts @@ -1,13 +1,13 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; -import { Observable } from 'rxjs'; -import { DetailService } from 'app/modules/detail/detail.service'; +import {Injectable} from '@angular/core'; +import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router'; +import {Observable} from 'rxjs'; +import {DetailService} from 'app/modules/detail/detail.service'; +import {DeviceDetailsResponseWrapper} from 'app/core/models/device-details-response-wrapper'; @Injectable({ providedIn: 'root' }) -export class DetailResolver implements Resolve -{ +export class DetailResolver implements Resolve { /** * Constructor * @@ -29,8 +29,7 @@ export class DetailResolver implements Resolve * @param route * @param state */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable - { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { return this._detailService.getData(route.params.wwn); } } diff --git a/webapp/frontend/src/app/modules/detail/detail.service.spec.ts b/webapp/frontend/src/app/modules/detail/detail.service.spec.ts new file mode 100644 index 0000000..ec6a4b9 --- /dev/null +++ b/webapp/frontend/src/app/modules/detail/detail.service.spec.ts @@ -0,0 +1,28 @@ +import {HttpClient} from '@angular/common/http'; +import {DetailService} from './detail.service'; +import {of} from 'rxjs'; +import {sda} from 'app/data/mock/device/details/sda' +import {DeviceDetailsResponseWrapper} from 'app/core/models/device-details-response-wrapper'; + +describe('DetailService', () => { + describe('#getData', () => { + let service: DetailService; + let httpClientSpy: jasmine.SpyObj; + + beforeEach(() => { + httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']); + service = new DetailService(httpClientSpy); + }); + it('should return getData() (HttpClient called once)', (done: DoneFn) => { + httpClientSpy.get.and.returnValue(of(sda)); + + service.getData('test').subscribe(value => { + expect(value).toBe(sda as DeviceDetailsResponseWrapper); + done(); + }); + expect(httpClientSpy.get.calls.count()) + .withContext('one call') + .toBe(1); + }); + }) +}); diff --git a/webapp/frontend/src/app/modules/detail/detail.service.ts b/webapp/frontend/src/app/modules/detail/detail.service.ts index 5747571..e75cb8b 100644 --- a/webapp/frontend/src/app/modules/detail/detail.service.ts +++ b/webapp/frontend/src/app/modules/detail/detail.service.ts @@ -1,16 +1,16 @@ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { getBasePath } from 'app/app.routing'; +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {BehaviorSubject, Observable} from 'rxjs'; +import {tap} from 'rxjs/operators'; +import {getBasePath} from 'app/app.routing'; +import {DeviceDetailsResponseWrapper} from 'app/core/models/device-details-response-wrapper'; @Injectable({ providedIn: 'root' }) -export class DetailService -{ +export class DetailService { // Observables - private _data: BehaviorSubject; + private _data: BehaviorSubject; /** * Constructor @@ -19,8 +19,7 @@ export class DetailService */ constructor( private _httpClient: HttpClient - ) - { + ) { // Set the private defaults this._data = new BehaviorSubject(null); } @@ -32,8 +31,7 @@ export class DetailService /** * Getter for data */ - get data$(): Observable - { + get data$(): Observable { return this._data.asObservable(); } @@ -44,10 +42,9 @@ export class DetailService /** * Get data */ - getData(wwn): Observable - { + getData(wwn): Observable { return this._httpClient.get(getBasePath() + `/api/device/${wwn}/details`).pipe( - tap((response: any) => { + tap((response: DeviceDetailsResponseWrapper) => { this._data.next(response); }) ); diff --git a/webapp/frontend/src/app/shared/device-title.pipe.spec.ts b/webapp/frontend/src/app/shared/device-title.pipe.spec.ts index 0661e84..992cf9a 100644 --- a/webapp/frontend/src/app/shared/device-title.pipe.spec.ts +++ b/webapp/frontend/src/app/shared/device-title.pipe.spec.ts @@ -1,14 +1,13 @@ -import { DeviceTitlePipe } from './device-title.pipe'; -import {FileSizePipe} from "./file-size.pipe"; -import {DeviceModel} from "../core/models/device-model"; +import {DeviceTitlePipe} from './device-title.pipe'; +import {DeviceModel} from 'app/core/models/device-model'; describe('DeviceTitlePipe', () => { - it('create an instance', () => { - const pipe = new DeviceTitlePipe(); - expect(pipe).toBeTruthy(); - }); + it('create an instance', () => { + const pipe = new DeviceTitlePipe(); + expect(pipe).toBeTruthy(); + }); - describe('#deviceTitleForType',() => { + describe('#deviceTitleForType', () => { const testCases = [ { 'device': { diff --git a/webapp/frontend/src/app/shared/device-title.pipe.ts b/webapp/frontend/src/app/shared/device-title.pipe.ts index 0c873f8..3cabc0f 100644 --- a/webapp/frontend/src/app/shared/device-title.pipe.ts +++ b/webapp/frontend/src/app/shared/device-title.pipe.ts @@ -1,4 +1,4 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; import {DeviceModel} from 'app/core/models/device-model'; @Pipe({ @@ -36,7 +36,7 @@ export class DeviceTitlePipe implements PipeTransform { return titleParts.join(' - ') } - static deviceTitleWithFallback(device, titleType: string): string { + static deviceTitleWithFallback(device: DeviceModel, titleType: string): string { console.log(`Displaying Device ${device.wwn} with: ${titleType}`) const titleParts = [] if (device.host_id) titleParts.push(device.host_id) @@ -48,7 +48,7 @@ export class DeviceTitlePipe implements PipeTransform { } - transform(device: any, titleType: string = 'name'): string { + transform(device: DeviceModel, titleType: string = 'name'): string { return DeviceTitlePipe.deviceTitleWithFallback(device, titleType) } From 9316eccabede90882fed206aed42f5f0d99f3a9c Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Sat, 9 Jul 2022 08:48:36 -0700 Subject: [PATCH 03/14] adding tests for tasks and aggregation queries (temp). --- .../scrutiny_repository_tasks_test.go | 149 ++++++++++++++ .../scrutiny_repository_temperature_test.go | 185 ++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 webapp/backend/pkg/database/scrutiny_repository_tasks_test.go create mode 100644 webapp/backend/pkg/database/scrutiny_repository_temperature_test.go diff --git a/webapp/backend/pkg/database/scrutiny_repository_tasks_test.go b/webapp/backend/pkg/database/scrutiny_repository_tasks_test.go new file mode 100644 index 0000000..487a1bc --- /dev/null +++ b/webapp/backend/pkg/database/scrutiny_repository_tasks_test.go @@ -0,0 +1,149 @@ +package database + +import ( + mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + "testing" +) + +func Test_DownsampleScript_Weekly(t *testing.T) { + t.Parallel() + + //setup + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeConfig := mock_config.NewMockInterface(mockCtrl) + fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() + + deviceRepo := scrutinyRepository{ + appConfig: fakeConfig, + } + + aggregationType := "weekly" + + //test + influxDbScript := deviceRepo.DownsampleScript(aggregationType) + + //assert + require.Equal(t, ` + sourceBucket = "metrics" + rangeStart = -2w + rangeEnd = -1w + aggWindow = 1w + destBucket = "metrics_weekly" + destOrg = "scrutiny" + + from(bucket: sourceBucket) + |> range(start: rangeStart, stop: rangeEnd) + |> filter(fn: (r) => r["_measurement"] == "smart" ) + |> group(columns: ["device_wwn", "_field"]) + |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) + |> to(bucket: destBucket, org: destOrg) + + temp_data = from(bucket: sourceBucket) + |> range(start: rangeStart, stop: rangeEnd) + |> filter(fn: (r) => r["_measurement"] == "temp") + |> group(columns: ["device_wwn"]) + |> toInt() + + temp_data + |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) + |> to(bucket: destBucket, org: destOrg) + `, influxDbScript) +} + +func Test_DownsampleScript_Monthly(t *testing.T) { + t.Parallel() + + //setup + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeConfig := mock_config.NewMockInterface(mockCtrl) + fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() + + deviceRepo := scrutinyRepository{ + appConfig: fakeConfig, + } + + aggregationType := "monthly" + + //test + influxDbScript := deviceRepo.DownsampleScript(aggregationType) + + //assert + require.Equal(t, ` + sourceBucket = "metrics_weekly" + rangeStart = -2mo + rangeEnd = -1mo + aggWindow = 1mo + destBucket = "metrics_monthly" + destOrg = "scrutiny" + + from(bucket: sourceBucket) + |> range(start: rangeStart, stop: rangeEnd) + |> filter(fn: (r) => r["_measurement"] == "smart" ) + |> group(columns: ["device_wwn", "_field"]) + |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) + |> to(bucket: destBucket, org: destOrg) + + temp_data = from(bucket: sourceBucket) + |> range(start: rangeStart, stop: rangeEnd) + |> filter(fn: (r) => r["_measurement"] == "temp") + |> group(columns: ["device_wwn"]) + |> toInt() + + temp_data + |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) + |> to(bucket: destBucket, org: destOrg) + `, influxDbScript) +} + +func Test_DownsampleScript_Yearly(t *testing.T) { + t.Parallel() + + //setup + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeConfig := mock_config.NewMockInterface(mockCtrl) + fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() + + deviceRepo := scrutinyRepository{ + appConfig: fakeConfig, + } + + aggregationType := "yearly" + + //test + influxDbScript := deviceRepo.DownsampleScript(aggregationType) + + //assert + require.Equal(t, ` + sourceBucket = "metrics_monthly" + rangeStart = -2y + rangeEnd = -1y + aggWindow = 1y + destBucket = "metrics_yearly" + destOrg = "scrutiny" + + from(bucket: sourceBucket) + |> range(start: rangeStart, stop: rangeEnd) + |> filter(fn: (r) => r["_measurement"] == "smart" ) + |> group(columns: ["device_wwn", "_field"]) + |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) + |> to(bucket: destBucket, org: destOrg) + + temp_data = from(bucket: sourceBucket) + |> range(start: rangeStart, stop: rangeEnd) + |> filter(fn: (r) => r["_measurement"] == "temp") + |> group(columns: ["device_wwn"]) + |> toInt() + + temp_data + |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) + |> to(bucket: destBucket, org: destOrg) + `, influxDbScript) +} diff --git a/webapp/backend/pkg/database/scrutiny_repository_temperature_test.go b/webapp/backend/pkg/database/scrutiny_repository_temperature_test.go new file mode 100644 index 0000000..7751ddf --- /dev/null +++ b/webapp/backend/pkg/database/scrutiny_repository_temperature_test.go @@ -0,0 +1,185 @@ +package database + +import ( + mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + "testing" +) + +func Test_aggregateTempQuery_Week(t *testing.T) { + t.Parallel() + + //setup + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeConfig := mock_config.NewMockInterface(mockCtrl) + fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() + + deviceRepo := scrutinyRepository{ + appConfig: fakeConfig, + } + + aggregationType := DURATION_KEY_WEEK + + //test + influxDbScript := deviceRepo.aggregateTempQuery(aggregationType) + + //assert + require.Equal(t, `import "influxdata/influxdb/schema" +weekData = from(bucket: "metrics") +|> range(start: -1w, stop: now()) +|> filter(fn: (r) => r["_measurement"] == "temp" ) +|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) +|> group(columns: ["device_wwn"]) +|> toInt() + +weekData +|> schema.fieldsAsCols() +|> yield()`, influxDbScript) +} + +func Test_aggregateTempQuery_Month(t *testing.T) { + t.Parallel() + + //setup + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeConfig := mock_config.NewMockInterface(mockCtrl) + fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() + + deviceRepo := scrutinyRepository{ + appConfig: fakeConfig, + } + + aggregationType := DURATION_KEY_MONTH + + //test + influxDbScript := deviceRepo.aggregateTempQuery(aggregationType) + + //assert + require.Equal(t, `import "influxdata/influxdb/schema" +weekData = from(bucket: "metrics") +|> range(start: -1w, stop: now()) +|> filter(fn: (r) => r["_measurement"] == "temp" ) +|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) +|> group(columns: ["device_wwn"]) +|> toInt() + +monthData = from(bucket: "metrics_weekly") +|> range(start: -1mo, stop: -1w) +|> filter(fn: (r) => r["_measurement"] == "temp" ) +|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) +|> group(columns: ["device_wwn"]) +|> toInt() + +union(tables: [weekData, monthData]) +|> group(columns: ["device_wwn"]) +|> sort(columns: ["_time"], desc: false) +|> schema.fieldsAsCols()`, influxDbScript) +} + +func Test_aggregateTempQuery_Year(t *testing.T) { + t.Parallel() + + //setup + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeConfig := mock_config.NewMockInterface(mockCtrl) + fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() + + deviceRepo := scrutinyRepository{ + appConfig: fakeConfig, + } + + aggregationType := DURATION_KEY_YEAR + + //test + influxDbScript := deviceRepo.aggregateTempQuery(aggregationType) + + //assert + require.Equal(t, `import "influxdata/influxdb/schema" +weekData = from(bucket: "metrics") +|> range(start: -1w, stop: now()) +|> filter(fn: (r) => r["_measurement"] == "temp" ) +|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) +|> group(columns: ["device_wwn"]) +|> toInt() + +monthData = from(bucket: "metrics_weekly") +|> range(start: -1mo, stop: -1w) +|> filter(fn: (r) => r["_measurement"] == "temp" ) +|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) +|> group(columns: ["device_wwn"]) +|> toInt() + +yearData = from(bucket: "metrics_monthly") +|> range(start: -1y, stop: -1mo) +|> filter(fn: (r) => r["_measurement"] == "temp" ) +|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) +|> group(columns: ["device_wwn"]) +|> toInt() + +union(tables: [weekData, monthData, yearData]) +|> group(columns: ["device_wwn"]) +|> sort(columns: ["_time"], desc: false) +|> schema.fieldsAsCols()`, influxDbScript) +} + +func Test_aggregateTempQuery_Forever(t *testing.T) { + t.Parallel() + + //setup + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + fakeConfig := mock_config.NewMockInterface(mockCtrl) + fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes() + fakeConfig.EXPECT().GetString("web.influxdb.org").Return("scrutiny").AnyTimes() + + deviceRepo := scrutinyRepository{ + appConfig: fakeConfig, + } + + aggregationType := DURATION_KEY_FOREVER + + //test + influxDbScript := deviceRepo.aggregateTempQuery(aggregationType) + + //assert + require.Equal(t, `import "influxdata/influxdb/schema" +weekData = from(bucket: "metrics") +|> range(start: -1w, stop: now()) +|> filter(fn: (r) => r["_measurement"] == "temp" ) +|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) +|> group(columns: ["device_wwn"]) +|> toInt() + +monthData = from(bucket: "metrics_weekly") +|> range(start: -1mo, stop: -1w) +|> filter(fn: (r) => r["_measurement"] == "temp" ) +|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) +|> group(columns: ["device_wwn"]) +|> toInt() + +yearData = from(bucket: "metrics_monthly") +|> range(start: -1y, stop: -1mo) +|> filter(fn: (r) => r["_measurement"] == "temp" ) +|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) +|> group(columns: ["device_wwn"]) +|> toInt() + +foreverData = from(bucket: "metrics_yearly") +|> range(start: -10y, stop: -1y) +|> filter(fn: (r) => r["_measurement"] == "temp" ) +|> aggregateWindow(every: 1h, fn: mean, createEmpty: false) +|> group(columns: ["device_wwn"]) +|> toInt() + +union(tables: [weekData, monthData, yearData, foreverData]) +|> group(columns: ["device_wwn"]) +|> sort(columns: ["_time"], desc: false) +|> schema.fieldsAsCols()`, influxDbScript) +} From 04563c0d0d1f07137e0dc8bf60895fde1867140f Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Sat, 9 Jul 2022 10:05:48 -0700 Subject: [PATCH 04/14] ensure we have the ability to keep influxdb tasks up-to-date. --- .../pkg/database/scrutiny_repository_tasks.go | 48 ++++++++++++++++--- .../scrutiny_repository_tasks_test.go | 18 +++---- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/webapp/backend/pkg/database/scrutiny_repository_tasks.go b/webapp/backend/pkg/database/scrutiny_repository_tasks.go index 079caff..92b67f4 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_tasks.go +++ b/webapp/backend/pkg/database/scrutiny_repository_tasks.go @@ -11,30 +11,66 @@ import ( //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// func (sr *scrutinyRepository) EnsureTasks(ctx context.Context, orgID string) error { weeklyTaskName := "tsk-weekly-aggr" + weeklyTaskScript := sr.DownsampleScript("weekly") if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: weeklyTaskName}); findErr == nil && len(found) == 0 { //weekly on Sunday at 1:00am - _, err := sr.influxTaskApi.CreateTaskWithCron(ctx, weeklyTaskName, sr.DownsampleScript("weekly"), "0 1 * * 0", orgID) + _, err := sr.influxTaskApi.CreateTaskWithCron(ctx, weeklyTaskName, weeklyTaskScript, "0 1 * * 0", orgID) if err != nil { return err } + } else if len(found) == 1 { + //check if we should update + task := &found[0] + if weeklyTaskScript != task.Flux { + sr.logger.Infoln("updating weekly task script") + task.Flux = weeklyTaskScript + _, err := sr.influxTaskApi.UpdateTask(ctx, task) + if err != nil { + return err + } + } } monthlyTaskName := "tsk-monthly-aggr" + monthlyTaskScript := sr.DownsampleScript("monthly") if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: monthlyTaskName}); findErr == nil && len(found) == 0 { //monthly on first day of the month at 1:30am - _, err := sr.influxTaskApi.CreateTaskWithCron(ctx, monthlyTaskName, sr.DownsampleScript("monthly"), "30 1 1 * *", orgID) + _, err := sr.influxTaskApi.CreateTaskWithCron(ctx, monthlyTaskName, monthlyTaskScript, "30 1 1 * *", orgID) if err != nil { return err } + } else if len(found) == 1 { + //check if we should update + task := &found[0] + if monthlyTaskScript != task.Flux { + sr.logger.Infoln("updating monthly task script") + task.Flux = monthlyTaskScript + _, err := sr.influxTaskApi.UpdateTask(ctx, task) + if err != nil { + return err + } + } } yearlyTaskName := "tsk-yearly-aggr" + yearlyTaskScript := sr.DownsampleScript("yearly") if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: yearlyTaskName}); findErr == nil && len(found) == 0 { //yearly on the first day of the year at 2:00am - _, err := sr.influxTaskApi.CreateTaskWithCron(ctx, yearlyTaskName, sr.DownsampleScript("yearly"), "0 2 1 1 *", orgID) + _, err := sr.influxTaskApi.CreateTaskWithCron(ctx, yearlyTaskName, yearlyTaskScript, "0 2 1 1 *", orgID) if err != nil { return err } + } else if len(found) == 1 { + //check if we should update + task := &found[0] + if yearlyTaskScript != task.Flux { + sr.logger.Infoln("updating yearly task script") + task.Flux = yearlyTaskScript + _, err := sr.influxTaskApi.UpdateTask(ctx, task) + if err != nil { + return err + } + } } return nil } @@ -102,14 +138,14 @@ func (sr *scrutinyRepository) DownsampleScript(aggregationType string) string { |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) |> to(bucket: destBucket, org: destOrg) - temp_data = from(bucket: sourceBucket) + from(bucket: sourceBucket) |> range(start: rangeStart, stop: rangeEnd) |> filter(fn: (r) => r["_measurement"] == "temp") |> group(columns: ["device_wwn"]) |> toInt() - - temp_data |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) + |> set(key: "_measurement", value: "temp") + |> set(key: "_field", value: "temp") |> to(bucket: destBucket, org: destOrg) `, sourceBucket, diff --git a/webapp/backend/pkg/database/scrutiny_repository_tasks_test.go b/webapp/backend/pkg/database/scrutiny_repository_tasks_test.go index 487a1bc..4f2dc51 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_tasks_test.go +++ b/webapp/backend/pkg/database/scrutiny_repository_tasks_test.go @@ -42,14 +42,14 @@ func Test_DownsampleScript_Weekly(t *testing.T) { |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) |> to(bucket: destBucket, org: destOrg) - temp_data = from(bucket: sourceBucket) + from(bucket: sourceBucket) |> range(start: rangeStart, stop: rangeEnd) |> filter(fn: (r) => r["_measurement"] == "temp") |> group(columns: ["device_wwn"]) |> toInt() - - temp_data |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) + |> set(key: "_measurement", value: "temp") + |> set(key: "_field", value: "temp") |> to(bucket: destBucket, org: destOrg) `, influxDbScript) } @@ -89,14 +89,14 @@ func Test_DownsampleScript_Monthly(t *testing.T) { |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) |> to(bucket: destBucket, org: destOrg) - temp_data = from(bucket: sourceBucket) + from(bucket: sourceBucket) |> range(start: rangeStart, stop: rangeEnd) |> filter(fn: (r) => r["_measurement"] == "temp") |> group(columns: ["device_wwn"]) |> toInt() - - temp_data |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) + |> set(key: "_measurement", value: "temp") + |> set(key: "_field", value: "temp") |> to(bucket: destBucket, org: destOrg) `, influxDbScript) } @@ -136,14 +136,14 @@ func Test_DownsampleScript_Yearly(t *testing.T) { |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) |> to(bucket: destBucket, org: destOrg) - temp_data = from(bucket: sourceBucket) + from(bucket: sourceBucket) |> range(start: rangeStart, stop: rangeEnd) |> filter(fn: (r) => r["_measurement"] == "temp") |> group(columns: ["device_wwn"]) |> toInt() - - temp_data |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) + |> set(key: "_measurement", value: "temp") + |> set(key: "_field", value: "temp") |> to(bucket: destBucket, org: destOrg) `, influxDbScript) } From 0f0efac866e673e33a55eb3cdc5e22e85802ba34 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Sat, 9 Jul 2022 10:42:30 -0700 Subject: [PATCH 05/14] fix update, using raw flux script. --- .../pkg/database/scrutiny_repository_tasks.go | 63 ++++--- .../scrutiny_repository_tasks_test.go | 159 ++++++++++-------- 2 files changed, 122 insertions(+), 100 deletions(-) diff --git a/webapp/backend/pkg/database/scrutiny_repository_tasks.go b/webapp/backend/pkg/database/scrutiny_repository_tasks.go index 92b67f4..82b6040 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_tasks.go +++ b/webapp/backend/pkg/database/scrutiny_repository_tasks.go @@ -11,10 +11,10 @@ import ( //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// func (sr *scrutinyRepository) EnsureTasks(ctx context.Context, orgID string) error { weeklyTaskName := "tsk-weekly-aggr" - weeklyTaskScript := sr.DownsampleScript("weekly") + weeklyTaskScript := sr.DownsampleScript("weekly", weeklyTaskName, "0 1 * * 0") if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: weeklyTaskName}); findErr == nil && len(found) == 0 { //weekly on Sunday at 1:00am - _, err := sr.influxTaskApi.CreateTaskWithCron(ctx, weeklyTaskName, weeklyTaskScript, "0 1 * * 0", orgID) + _, err := sr.influxTaskApi.CreateTaskByFlux(ctx, weeklyTaskScript, orgID) if err != nil { return err } @@ -32,10 +32,10 @@ func (sr *scrutinyRepository) EnsureTasks(ctx context.Context, orgID string) err } monthlyTaskName := "tsk-monthly-aggr" - monthlyTaskScript := sr.DownsampleScript("monthly") + monthlyTaskScript := sr.DownsampleScript("monthly", monthlyTaskName, "30 1 1 * *") if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: monthlyTaskName}); findErr == nil && len(found) == 0 { //monthly on first day of the month at 1:30am - _, err := sr.influxTaskApi.CreateTaskWithCron(ctx, monthlyTaskName, monthlyTaskScript, "30 1 1 * *", orgID) + _, err := sr.influxTaskApi.CreateTaskByFlux(ctx, monthlyTaskScript, orgID) if err != nil { return err } @@ -53,10 +53,10 @@ func (sr *scrutinyRepository) EnsureTasks(ctx context.Context, orgID string) err } yearlyTaskName := "tsk-yearly-aggr" - yearlyTaskScript := sr.DownsampleScript("yearly") + yearlyTaskScript := sr.DownsampleScript("yearly", yearlyTaskName, "0 2 1 1 *") if found, findErr := sr.influxTaskApi.FindTasks(ctx, &api.TaskFilter{Name: yearlyTaskName}); findErr == nil && len(found) == 0 { //yearly on the first day of the year at 2:00am - _, err := sr.influxTaskApi.CreateTaskWithCron(ctx, yearlyTaskName, yearlyTaskScript, "0 2 1 1 *", orgID) + _, err := sr.influxTaskApi.CreateTaskByFlux(ctx, yearlyTaskScript, orgID) if err != nil { return err } @@ -75,7 +75,7 @@ func (sr *scrutinyRepository) EnsureTasks(ctx context.Context, orgID string) err return nil } -func (sr *scrutinyRepository) DownsampleScript(aggregationType string) string { +func (sr *scrutinyRepository) DownsampleScript(aggregationType string, name string, cron string) string { var sourceBucket string // the source of the data var destBucket string // the destination for the aggregated data var rangeStart string @@ -124,30 +124,37 @@ func (sr *scrutinyRepository) DownsampleScript(aggregationType string) string { */ return fmt.Sprintf(` - sourceBucket = "%s" - rangeStart = %s - rangeEnd = %s - aggWindow = %s - destBucket = "%s" - destOrg = "%s" +option task = { + name: "%s", + cron: "%s", +} + +sourceBucket = "%s" +rangeStart = %s +rangeEnd = %s +aggWindow = %s +destBucket = "%s" +destOrg = "%s" - from(bucket: sourceBucket) - |> range(start: rangeStart, stop: rangeEnd) - |> filter(fn: (r) => r["_measurement"] == "smart" ) - |> group(columns: ["device_wwn", "_field"]) - |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) - |> to(bucket: destBucket, org: destOrg) +from(bucket: sourceBucket) +|> range(start: rangeStart, stop: rangeEnd) +|> filter(fn: (r) => r["_measurement"] == "smart" ) +|> group(columns: ["device_wwn", "_field"]) +|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) +|> to(bucket: destBucket, org: destOrg) - from(bucket: sourceBucket) - |> range(start: rangeStart, stop: rangeEnd) - |> filter(fn: (r) => r["_measurement"] == "temp") - |> group(columns: ["device_wwn"]) - |> toInt() - |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) - |> set(key: "_measurement", value: "temp") - |> set(key: "_field", value: "temp") - |> to(bucket: destBucket, org: destOrg) +from(bucket: sourceBucket) +|> range(start: rangeStart, stop: rangeEnd) +|> filter(fn: (r) => r["_measurement"] == "temp") +|> group(columns: ["device_wwn"]) +|> toInt() +|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) +|> set(key: "_measurement", value: "temp") +|> set(key: "_field", value: "temp") +|> to(bucket: destBucket, org: destOrg) `, + name, + cron, sourceBucket, rangeStart, rangeEnd, diff --git a/webapp/backend/pkg/database/scrutiny_repository_tasks_test.go b/webapp/backend/pkg/database/scrutiny_repository_tasks_test.go index 4f2dc51..e1e5e5c 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_tasks_test.go +++ b/webapp/backend/pkg/database/scrutiny_repository_tasks_test.go @@ -24,33 +24,38 @@ func Test_DownsampleScript_Weekly(t *testing.T) { aggregationType := "weekly" //test - influxDbScript := deviceRepo.DownsampleScript(aggregationType) + influxDbScript := deviceRepo.DownsampleScript(aggregationType, "tsk-weekly-aggr", "0 1 * * 0") //assert require.Equal(t, ` - sourceBucket = "metrics" - rangeStart = -2w - rangeEnd = -1w - aggWindow = 1w - destBucket = "metrics_weekly" - destOrg = "scrutiny" - - from(bucket: sourceBucket) - |> range(start: rangeStart, stop: rangeEnd) - |> filter(fn: (r) => r["_measurement"] == "smart" ) - |> group(columns: ["device_wwn", "_field"]) - |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) - |> to(bucket: destBucket, org: destOrg) - - from(bucket: sourceBucket) - |> range(start: rangeStart, stop: rangeEnd) - |> filter(fn: (r) => r["_measurement"] == "temp") - |> group(columns: ["device_wwn"]) - |> toInt() - |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) - |> set(key: "_measurement", value: "temp") - |> set(key: "_field", value: "temp") - |> to(bucket: destBucket, org: destOrg) +option task = { + name: "tsk-weekly-aggr", + cron: "0 1 * * 0", +} + +sourceBucket = "metrics" +rangeStart = -2w +rangeEnd = -1w +aggWindow = 1w +destBucket = "metrics_weekly" +destOrg = "scrutiny" + +from(bucket: sourceBucket) +|> range(start: rangeStart, stop: rangeEnd) +|> filter(fn: (r) => r["_measurement"] == "smart" ) +|> group(columns: ["device_wwn", "_field"]) +|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) +|> to(bucket: destBucket, org: destOrg) + +from(bucket: sourceBucket) +|> range(start: rangeStart, stop: rangeEnd) +|> filter(fn: (r) => r["_measurement"] == "temp") +|> group(columns: ["device_wwn"]) +|> toInt() +|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) +|> set(key: "_measurement", value: "temp") +|> set(key: "_field", value: "temp") +|> to(bucket: destBucket, org: destOrg) `, influxDbScript) } @@ -71,33 +76,38 @@ func Test_DownsampleScript_Monthly(t *testing.T) { aggregationType := "monthly" //test - influxDbScript := deviceRepo.DownsampleScript(aggregationType) + influxDbScript := deviceRepo.DownsampleScript(aggregationType, "tsk-monthly-aggr", "30 1 1 * *") //assert require.Equal(t, ` - sourceBucket = "metrics_weekly" - rangeStart = -2mo - rangeEnd = -1mo - aggWindow = 1mo - destBucket = "metrics_monthly" - destOrg = "scrutiny" - - from(bucket: sourceBucket) - |> range(start: rangeStart, stop: rangeEnd) - |> filter(fn: (r) => r["_measurement"] == "smart" ) - |> group(columns: ["device_wwn", "_field"]) - |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) - |> to(bucket: destBucket, org: destOrg) - - from(bucket: sourceBucket) - |> range(start: rangeStart, stop: rangeEnd) - |> filter(fn: (r) => r["_measurement"] == "temp") - |> group(columns: ["device_wwn"]) - |> toInt() - |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) - |> set(key: "_measurement", value: "temp") - |> set(key: "_field", value: "temp") - |> to(bucket: destBucket, org: destOrg) +option task = { + name: "tsk-monthly-aggr", + cron: "30 1 1 * *", +} + +sourceBucket = "metrics_weekly" +rangeStart = -2mo +rangeEnd = -1mo +aggWindow = 1mo +destBucket = "metrics_monthly" +destOrg = "scrutiny" + +from(bucket: sourceBucket) +|> range(start: rangeStart, stop: rangeEnd) +|> filter(fn: (r) => r["_measurement"] == "smart" ) +|> group(columns: ["device_wwn", "_field"]) +|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) +|> to(bucket: destBucket, org: destOrg) + +from(bucket: sourceBucket) +|> range(start: rangeStart, stop: rangeEnd) +|> filter(fn: (r) => r["_measurement"] == "temp") +|> group(columns: ["device_wwn"]) +|> toInt() +|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) +|> set(key: "_measurement", value: "temp") +|> set(key: "_field", value: "temp") +|> to(bucket: destBucket, org: destOrg) `, influxDbScript) } @@ -118,32 +128,37 @@ func Test_DownsampleScript_Yearly(t *testing.T) { aggregationType := "yearly" //test - influxDbScript := deviceRepo.DownsampleScript(aggregationType) + influxDbScript := deviceRepo.DownsampleScript(aggregationType, "tsk-yearly-aggr", "0 2 1 1 *") //assert require.Equal(t, ` - sourceBucket = "metrics_monthly" - rangeStart = -2y - rangeEnd = -1y - aggWindow = 1y - destBucket = "metrics_yearly" - destOrg = "scrutiny" - - from(bucket: sourceBucket) - |> range(start: rangeStart, stop: rangeEnd) - |> filter(fn: (r) => r["_measurement"] == "smart" ) - |> group(columns: ["device_wwn", "_field"]) - |> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) - |> to(bucket: destBucket, org: destOrg) - - from(bucket: sourceBucket) - |> range(start: rangeStart, stop: rangeEnd) - |> filter(fn: (r) => r["_measurement"] == "temp") - |> group(columns: ["device_wwn"]) - |> toInt() - |> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) - |> set(key: "_measurement", value: "temp") - |> set(key: "_field", value: "temp") - |> to(bucket: destBucket, org: destOrg) +option task = { + name: "tsk-yearly-aggr", + cron: "0 2 1 1 *", +} + +sourceBucket = "metrics_monthly" +rangeStart = -2y +rangeEnd = -1y +aggWindow = 1y +destBucket = "metrics_yearly" +destOrg = "scrutiny" + +from(bucket: sourceBucket) +|> range(start: rangeStart, stop: rangeEnd) +|> filter(fn: (r) => r["_measurement"] == "smart" ) +|> group(columns: ["device_wwn", "_field"]) +|> aggregateWindow(every: aggWindow, fn: last, createEmpty: false) +|> to(bucket: destBucket, org: destOrg) + +from(bucket: sourceBucket) +|> range(start: rangeStart, stop: rangeEnd) +|> filter(fn: (r) => r["_measurement"] == "temp") +|> group(columns: ["device_wwn"]) +|> toInt() +|> aggregateWindow(fn: mean, every: aggWindow, createEmpty: false) +|> set(key: "_measurement", value: "temp") +|> set(key: "_field", value: "temp") +|> to(bucket: destBucket, org: destOrg) `, influxDbScript) } From 30bd18f816ddbdaffd3176d4c77671011fa3ba30 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Sat, 9 Jul 2022 17:00:51 -0700 Subject: [PATCH 06/14] updating docs. --- docs/SUPPORTED_NAS_OS.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/SUPPORTED_NAS_OS.md b/docs/SUPPORTED_NAS_OS.md index 455392f..65740a9 100644 --- a/docs/SUPPORTED_NAS_OS.md +++ b/docs/SUPPORTED_NAS_OS.md @@ -1,17 +1,17 @@ # Officially Supported NAS OS's -These are the officially supported NAS OS's (with documentation and setup guides). -Once a guide is created (in `docs/guides/`) it will be linked here. +These are the officially supported NAS OS's (with documentation and setup guides). Once a guide is created ( +in `docs/guides/` or elsewhere) it will be linked here. -- [ ] freenas/truenas +- [x] [freenas/truenas](https://blog.stefandroid.com/2022/01/14/smart-scrutiny.html) - [x] [unraid](./INSTALL_UNRAID.md) - [ ] ESXI - [ ] Proxmox -- [x] Synology(./INSTALL_SYNOLOGY_COLLECTOR.md) +- [x] [Synology](./INSTALL_SYNOLOGY_COLLECTOR.md) - [ ] OMV - [ ] Amahi - [ ] Running in a LXC container - [x] [PFSense](./INSTALL_UNRAID.md) -- [ ] QNAP -- [ ] RockStor +- [x] QNAP +- [x] [RockStor](https://rockstor.com/docs/interface/docker-based-rock-ons/scrutiny.html) From 5ea149d8784f8802d8ed4f484b7f824e7ea02e08 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Sat, 9 Jul 2022 18:28:49 -0700 Subject: [PATCH 07/14] upgrading to go 1.18 for generics (and lodash-like library). devices with an empty wwn should be filtered out (not uploaded during device registration, skipped when attempting to upload metrics). added a migration to delete existing device entries with an empty `wwn` fixes #314 --- .github/workflows/ci.yaml | 3 +++ CONTRIBUTING.md | 7 ++++--- collector/pkg/collector/metrics.go | 8 +++++++- docker/Dockerfile | 2 +- docker/Dockerfile.collector | 2 +- docker/Dockerfile.web | 2 +- go.mod | 7 ++++--- go.sum | 17 ++++++++++------- .../database/scrutiny_repository_migrations.go | 8 ++++++++ .../backend/pkg/web/handler/register_devices.go | 10 ++++++++-- .../pkg/web/handler/upload_device_metrics.go | 4 ++++ 11 files changed, 51 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 233fa26..211a579 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -99,6 +99,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 + - uses: actions/setup-go@v3 + with: + go-version: '^1.18.3' - name: Build Binaries run: | make binary-clean binary-all diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c665e9c..5a6ad71 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,8 +9,9 @@ Depending on the functionality you are adding, you may need to setup a developme # Modifying the Scrutiny Backend Server (API) -1. install the [Go runtime](https://go.dev/doc/install) (v1.17+) -2. download the `scrutiny-web-frontend.tar.gz` for the [latest release](https://github.com/AnalogJ/scrutiny/releases/latest). Extract to a folder named `dist` +1. install the [Go runtime](https://go.dev/doc/install) (v1.18+) +2. download the `scrutiny-web-frontend.tar.gz` for + the [latest release](https://github.com/AnalogJ/scrutiny/releases/latest). Extract to a folder named `dist` 3. create a `scrutiny.yaml` config file ```yaml # config file for local development. store as scrutiny.yaml @@ -62,7 +63,7 @@ The frontend is written in Angular. If you're working on the frontend and can us If you're developing a feature that requires changes to the backend and the frontend, or a frontend feature that requires real data, you'll need to follow the steps below: -1. install the [Go runtime](https://go.dev/doc/install) (v1.17+) +1. install the [Go runtime](https://go.dev/doc/install) (v1.18+) 2. install [NodeJS](https://nodejs.org/en/download/) 3. create a `scrutiny.yaml` config file ```yaml diff --git a/collector/pkg/collector/metrics.go b/collector/pkg/collector/metrics.go index 415516b..5d453dd 100644 --- a/collector/pkg/collector/metrics.go +++ b/collector/pkg/collector/metrics.go @@ -9,6 +9,7 @@ import ( "github.com/analogj/scrutiny/collector/pkg/detect" "github.com/analogj/scrutiny/collector/pkg/errors" "github.com/analogj/scrutiny/collector/pkg/models" + "github.com/samber/lo" "github.com/sirupsen/logrus" "net/url" "os" @@ -56,11 +57,16 @@ func (mc *MetricsCollector) Run() error { Logger: mc.logger, Config: mc.config, } - detectedStorageDevices, err := deviceDetector.Start() + rawDetectedStorageDevices, err := deviceDetector.Start() if err != nil { return err } + //filter any device with empty wwn (they are invalid) + detectedStorageDevices := lo.Filter[models.Device](rawDetectedStorageDevices, func(dev models.Device, _ int) bool { + return len(dev.WWN) > 0 + }) + mc.logger.Infoln("Sending detected devices to API, for filtering & validation") jsonObj, _ := json.Marshal(detectedStorageDevices) mc.logger.Debugf("Detected devices: %v", string(jsonObj)) diff --git a/docker/Dockerfile b/docker/Dockerfile index c6074da..7d35bef 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,7 +5,7 @@ ######## -FROM golang:1.17-bullseye as backendbuild +FROM golang:1.18-bullseye as backendbuild WORKDIR /go/src/github.com/analogj/scrutiny COPY . /go/src/github.com/analogj/scrutiny diff --git a/docker/Dockerfile.collector b/docker/Dockerfile.collector index 9c614f8..c4553fd 100644 --- a/docker/Dockerfile.collector +++ b/docker/Dockerfile.collector @@ -4,7 +4,7 @@ ######## -FROM golang:1.17-bullseye as backendbuild +FROM golang:1.18-bullseye as backendbuild WORKDIR /go/src/github.com/analogj/scrutiny diff --git a/docker/Dockerfile.web b/docker/Dockerfile.web index ff03115..8d1192c 100644 --- a/docker/Dockerfile.web +++ b/docker/Dockerfile.web @@ -5,7 +5,7 @@ ######## -FROM golang:1.17-bullseye as backendbuild +FROM golang:1.18-bullseye as backendbuild WORKDIR /go/src/github.com/analogj/scrutiny diff --git a/go.mod b/go.mod index 91e6062..5a6daf3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/analogj/scrutiny -go 1.17 +go 1.18 require ( github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14 @@ -13,6 +13,7 @@ require ( github.com/influxdata/influxdb-client-go/v2 v2.9.0 github.com/jaypipes/ghw v0.6.1 github.com/mitchellh/mapstructure v1.2.2 + github.com/samber/lo v1.25.0 github.com/sirupsen/logrus v1.4.2 github.com/spf13/viper v1.7.0 github.com/stretchr/testify v1.7.1 @@ -23,7 +24,6 @@ require ( require ( github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect - github.com/citilinkru/libudev v1.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deepmap/oapi-codegen v1.8.2 // indirect @@ -68,6 +68,7 @@ require ( github.com/subosito/gotenv v1.2.0 // indirect github.com/ugorji/go/codec v1.1.7 // indirect golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 // indirect golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect @@ -77,7 +78,7 @@ require ( gopkg.in/ini.v1 v1.55.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect gosrc.io/xmpp v0.5.1 // indirect howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect modernc.org/libc v1.16.8 // indirect diff --git a/go.sum b/go.sum index c8c79fc..e2bdeaa 100644 --- a/go.sum +++ b/go.sum @@ -39,8 +39,6 @@ github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVz github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM= github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4= -github.com/citilinkru/libudev v1.0.0 h1:upErSdhsJGdiKxwxPmvcz43fwJJD9R+y1j8BqU4wHog= -github.com/citilinkru/libudev v1.0.0/go.mod h1:yaNdhdtfJMs5flqeXzUOMO0mT9QnyNh/U/jdY4WhA/I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/containrrr/shoutrrr v0.4.4 h1:vHZ4E/76pKVY+Jyn/qhBz3X540Bn8NI5ppPHK4PyILY= @@ -282,12 +280,11 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5 h1:dkCjlgGN81ahDFtM9R1x16gFGTa7ZvgZfdtAfM9lWOs= github.com/kvz/logstreamer v0.0.0-20201023134116-02d20f4338f5/go.mod h1:8/LTPeDLaklcUjgSQBHbhBF1ibKAFxzS5o+H7USfMSA= github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= @@ -345,6 +342,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.6 h1:11TGpSHY7Esh/i/qnq02Jo5oVrI1Gue8Slbq0ujPZFQ= github.com/nxadm/tail v1.4.6/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -388,6 +386,8 @@ github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThC github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samber/lo v1.25.0 h1:H8F6cB0RotRdgcRCivTByAQePaYhGMdOTJIj2QFS2I0= +github.com/samber/lo v1.25.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= @@ -436,10 +436,10 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= @@ -485,6 +485,8 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -656,8 +658,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= @@ -675,8 +677,9 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.0.1 h1:omJoilUzyrAp0xNoio88lGJCroGdIOen9hq2A/+3ifw= gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw= gorm.io/driver/postgres v1.0.0 h1:Yh4jyFQ0a7F+JPU0Gtiam/eKmpT/XFc1FKxotGqc6FM= diff --git a/webapp/backend/pkg/database/scrutiny_repository_migrations.go b/webapp/backend/pkg/database/scrutiny_repository_migrations.go index ab3296e..bb40add 100644 --- a/webapp/backend/pkg/database/scrutiny_repository_migrations.go +++ b/webapp/backend/pkg/database/scrutiny_repository_migrations.go @@ -267,6 +267,14 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error { return tx.AutoMigrate(m20220509170100.Device{}) }, }, + { + ID: "m20220709181300", + Migrate: func(tx *gorm.DB) error { + + // delete devices with empty `wwn` field (they are impossible to delete manually), and are invalid. + return tx.Where("wwn = ?", "").Delete(&models.Device{}).Error + }, + }, }) if err := m.Migrate(); err != nil { diff --git a/webapp/backend/pkg/web/handler/register_devices.go b/webapp/backend/pkg/web/handler/register_devices.go index e1ddf95..cb0c59b 100644 --- a/webapp/backend/pkg/web/handler/register_devices.go +++ b/webapp/backend/pkg/web/handler/register_devices.go @@ -4,6 +4,7 @@ import ( "github.com/analogj/scrutiny/webapp/backend/pkg/database" "github.com/analogj/scrutiny/webapp/backend/pkg/models" "github.com/gin-gonic/gin" + "github.com/samber/lo" "github.com/sirupsen/logrus" "net/http" ) @@ -22,8 +23,13 @@ func RegisterDevices(c *gin.Context) { return } + //filter any device with empty wwn (they are invalid) + detectedStorageDevices := lo.Filter[models.Device](collectorDeviceWrapper.Data, func(dev models.Device, _ int) bool { + return len(dev.WWN) > 0 + }) + errs := []error{} - for _, dev := range collectorDeviceWrapper.Data { + for _, dev := range detectedStorageDevices { //insert devices into DB (and update specified columns if device is already registered) // update device fields that may change: (DeviceType, HostID) if err := deviceRepo.RegisterDevice(c, dev); err != nil { @@ -40,7 +46,7 @@ func RegisterDevices(c *gin.Context) { } else { c.JSON(http.StatusOK, models.DeviceWrapper{ Success: true, - Data: collectorDeviceWrapper.Data, + Data: detectedStorageDevices, }) return } diff --git a/webapp/backend/pkg/web/handler/upload_device_metrics.go b/webapp/backend/pkg/web/handler/upload_device_metrics.go index e893366..d27f66b 100644 --- a/webapp/backend/pkg/web/handler/upload_device_metrics.go +++ b/webapp/backend/pkg/web/handler/upload_device_metrics.go @@ -20,6 +20,10 @@ func UploadDeviceMetrics(c *gin.Context) { //appConfig := c.MustGet("CONFIG").(config.Interface) + if c.Param("wwn") == "" { + c.JSON(http.StatusBadRequest, gin.H{"success": false}) + } + var collectorSmartData collector.SmartInfo err := c.BindJSON(&collectorSmartData) if err != nil { From 2361c329e22b3078df59df96f8a62262d2fde6b4 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Sun, 10 Jul 2022 09:01:35 -0700 Subject: [PATCH 08/14] added USB instructions to trouble shooting guide. fixes #266 added solaris to supported os list. --- docs/SUPPORTED_NAS_OS.md | 5 +++-- docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md | 24 +++++++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/SUPPORTED_NAS_OS.md b/docs/SUPPORTED_NAS_OS.md index 65740a9..aae1e52 100644 --- a/docs/SUPPORTED_NAS_OS.md +++ b/docs/SUPPORTED_NAS_OS.md @@ -1,4 +1,4 @@ -# Officially Supported NAS OS's +# Officially Supported NAS/OS's These are the officially supported NAS OS's (with documentation and setup guides). Once a guide is created ( in `docs/guides/` or elsewhere) it will be linked here. @@ -14,4 +14,5 @@ in `docs/guides/` or elsewhere) it will be linked here. - [x] [PFSense](./INSTALL_UNRAID.md) - [x] QNAP - [x] [RockStor](https://rockstor.com/docs/interface/docker-based-rock-ons/scrutiny.html) - +- [ ] Solaris/OmniOS CE Support +- [ ] Kubernetes diff --git a/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md b/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md index dbea27a..d512aee 100644 --- a/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md +++ b/docs/TROUBLESHOOTING_DEVICE_COLLECTOR.md @@ -104,7 +104,7 @@ devices: As mentioned in the [README.md](/README.md), NVMe devices require both `--cap-add SYS_RAWIO` and `--cap-add SYS_ADMIN` to allow smartctl permission to query your NVMe device SMART data [#26](https://github.com/AnalogJ/scrutiny/issues/26) -When attaching NVMe devices using `--device=/dev/nvme..`, make sure to provide the device controller (`/dev/nvme0`) +When attaching NVMe devices using `--device=/dev/nvme..`, make sure to provide the device controller (`/dev/nvme0`) instead of the block device (`/dev/nvme0n1`). See [#209](https://github.com/AnalogJ/scrutiny/issues/209). > The character device /dev/nvme0 is the NVME device controller, and block devices like /dev/nvme0n1 are the NVME storage namespaces: the devices you use for actual storage, which will behave essentially as disks. @@ -113,15 +113,29 @@ instead of the block device (`/dev/nvme0n1`). See [#209](https://github.com/Anal ### ATA +### USB Devices + +The following information is extracted from [#266](https://github.com/AnalogJ/scrutiny/issues/266) + +External HDDs support two modes of operation usb-storage (old, slower, stable) and uas (new, faster, sometimes unstable) +. On some external HDDs, uas mode does not properly pass through SMART information, or even causes hardware issues, so +it has been disabled by the kernel. No amount of smartctl parameters will fix this, as it is being rejected by the +kernel. This is especially true with Seagate HDDs. One solution is to force these devices into usb-storage mode, which +will incur some performance penalty, but may work well enough for you. More info: + +- https://smartmontools.org/wiki/Supported_USB-Devices +- https://smartmontools.org/wiki/SAT-with-UAS-Linux +- https://forums.raspberrypi.com/viewtopic.php?t=245931 + ### Exit Codes If you see an error message similar to `smartctl returned an error code (2) while processing /dev/sda`, this means that -`smartctl` (not Scrutiny) exited with an error code. Scrutiny will attempt to print a helpful error message to help you debug, -but you can look at the table (and associated links) below to debug `smartctl`. +`smartctl` (not Scrutiny) exited with an error code. Scrutiny will attempt to print a helpful error message to help you +debug, but you can look at the table (and associated links) below to debug `smartctl`. > smartctl Return Values -> The return values of smartctl are defined by a bitmask. If all is well with the disk, the return value (exit status) of -> smartctl is 0 (all bits turned off). If a problem occurs, or an error, potential error, or fault is detected, then +> The return values of smartctl are defined by a bitmask. If all is well with the disk, the return value (exit status) of +> smartctl is 0 (all bits turned off). If a problem occurs, or an error, potential error, or fault is detected, then > a non-zero status is returned. In this case, the eight different bits in the return value have the following meanings > for ATA disks; some of these values may also be returned for SCSI disks. > From c6579864b8b190ce765d8ac996e8eab17ce78434 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Sun, 10 Jul 2022 11:30:38 -0700 Subject: [PATCH 09/14] added instructions for how to create a Scope restricted InfluxDB API token for use with Scrutiny. - fixes #249 --- docs/TROUBLESHOOTING_INFLUXDB.md | 315 +++++++++++++++++- .../pkg/database/scrutiny_repository.go | 12 +- 2 files changed, 321 insertions(+), 6 deletions(-) diff --git a/docs/TROUBLESHOOTING_INFLUXDB.md b/docs/TROUBLESHOOTING_INFLUXDB.md index 437042b..22b52c6 100644 --- a/docs/TROUBLESHOOTING_INFLUXDB.md +++ b/docs/TROUBLESHOOTING_INFLUXDB.md @@ -66,12 +66,319 @@ panic: failed to check influxdb setup status - parse "://:": missing protocol sc As discussed in [#248](https://github.com/AnalogJ/scrutiny/issues/248) and [#234](https://github.com/AnalogJ/scrutiny/issues/234), this usually related to either: -- Upgrading from the LSIO Scrutiny image to the Official Scrutiny image, without removing LSIO specific environmental variables - - remove the `SCRUTINY_WEB=true` and `SCRUTINY_COLLECTOR=true` environmental variables. They were used by the LSIO image, but are unnecessary and cause issues with the official Scrutiny image. -- Updated versions of the [LSIO Scrutiny images are broken](https://github.com/linuxserver/docker-scrutiny/issues/22), as they have not installed InfluxDB which is a required dependency of Scrutiny v0.4.x - - You can revert to an earlier version of the LSIO image (`lscr.io/linuxserver/scrutiny:060ac7b8-ls34`), or just change to the official Scrutiny image (`ghcr.io/analogj/scrutiny:master-omnibus`) +- Upgrading from the LSIO Scrutiny image to the Official Scrutiny image, without removing LSIO specific environmental + variables + - remove the `SCRUTINY_WEB=true` and `SCRUTINY_COLLECTOR=true` environmental variables. They were used by the LSIO + image, but are unnecessary and cause issues with the official Scrutiny image. +- Updated versions of the [LSIO Scrutiny images are broken](https://github.com/linuxserver/docker-scrutiny/issues/22), + as they have not installed InfluxDB which is a required dependency of Scrutiny v0.4.x + - You can revert to an earlier version of the LSIO image (`lscr.io/linuxserver/scrutiny:060ac7b8-ls34`), or just + change to the official Scrutiny image (`ghcr.io/analogj/scrutiny:master-omnibus`) Here's a couple of confirmed working docker-compose files that you may want to look at: - https://github.com/AnalogJ/scrutiny/blob/master/docker/example.hubspoke.docker-compose.yml - https://github.com/AnalogJ/scrutiny/blob/master/docker/example.omnibus.docker-compose.yml + +## Bring your own InfluxDB + +> WARNING: Most users should not follow these steps. This is ONLY for users who have an EXISTING InfluxDB installation which contains data from multiple services. +> The Scrutiny Docker omnibus image includes an empty InfluxDB instance which it can configure. +> If you're deploying manually or via Hub/Spoke, you can just follow the installation instructions, Scrutiny knows how +> to run the first-time setup automatically. + +The goal here is to create an InfluxDB API key with minimal permissions for use by Scrutiny. + +- Create Scrutiny buckets (`metrics`, `metrics_weekly`, `metrics_monthly`, `metrics_yearly`) with placeholder config +- Create Downsampling tasks (`tsk-weekly-aggr`, `tsk-monthly-aggr`, `tsk-yearly-aggr`) with placeholder script. +- Create API token with restricted scope +- NOTE: Placeholder bucket & task configuration will be replaced automatically by Scrutiny during startup + +The placeholder buckets and tasks need to be created before the API token can be created, as the resource ID's need to +exist for the scope restriction to work. + +Scopes: + +- `orgs`: read - required for scrutiny to find it's configured org_id +- `tasks`: scrutiny specific read/write access - Scrutiny only needs access to the downsampling tasks you created above +- `buckets`: scrutiny specific read/write access - Scrutiny only needs access to the buckets you created above + +### Setup Environmental Variables + +```bash +# replace the following values with correct values for your InfluxDB installation +export INFLUXDB_ADMIN_TOKEN=pCqRq7xxxxxx-FZgNLfstIs0w== +export INFLUXDB_ORG_ID=b2495xxxxx +export INFLUXDB_HOSTNAME=http://localhost:8086 + +# if you want to change the bucket name prefix below, you'll also need to update the setting in the scrutiny.yaml config file. +export INFLUXDB_SCRUTINY_BUCKET_BASENAME=metrics +``` + +### Create placeholder buckets + +
+ Click to expand! + +```bash +curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/buckets \ +-H "Content-Type: application/json" \ +-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \ +--data-binary @- << EOF +{ +"name": "${INFLUXDB_SCRUTINY_BUCKET_BASENAME}", +"orgID": "${INFLUXDB_ORG_ID}", +"retentionRules": [] +} +EOF + +curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/buckets \ +-H "Content-Type: application/json" \ +-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \ +--data-binary @- << EOF +{ +"name": "${INFLUXDB_SCRUTINY_BUCKET_BASENAME}_weekly", +"orgID": "${INFLUXDB_ORG_ID}", +"retentionRules": [] +} +EOF + +curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/buckets \ +-H "Content-Type: application/json" \ +-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \ +--data-binary @- << EOF +{ +"name": "${INFLUXDB_SCRUTINY_BUCKET_BASENAME}_monthly", +"orgID": "${INFLUXDB_ORG_ID}", +"retentionRules": [] +} +EOF + +curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/buckets \ +-H "Content-Type: application/json" \ +-H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \ +--data-binary @- << EOF +{ +"name": "${INFLUXDB_SCRUTINY_BUCKET_BASENAME}_yearly", +"orgID": "${INFLUXDB_ORG_ID}", +"retentionRules": [] +} +EOF +``` + +
+ +### Create placeholder tasks + +
+ Click to expand! + +```bash +curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/tasks \ + -H "Content-Type: application/json" \ + -H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \ + --data-binary @- << EOF +{ + "orgID": "${INFLUXDB_ORG_ID}", + "flux": "option task = {name: \"tsk-weekly-aggr\", every: 1y} \nyield now()" +} +EOF + +curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/tasks \ + -H "Content-Type: application/json" \ + -H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \ + --data-binary @- << EOF +{ + "orgID": "${INFLUXDB_ORG_ID}", + "flux": "option task = {name: \"tsk-monthly-aggr\", every: 1y} \nyield now()" +} +EOF + +curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/tasks \ + -H "Content-Type: application/json" \ + -H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \ + --data-binary @- << EOF +{ + "orgID": "${INFLUXDB_ORG_ID}", + "flux": "option task = {name: \"tsk-yearly-aggr\", every: 1y} \nyield now()" +} +EOF + +``` + +
+ +### Create InfluxDB API Token + +
+ Click to expand! + +```bash +# replace these values with placeholder bucket and task ids from your InfluxDB installation. +export INFLUXDB_SCRUTINY_BASE_BUCKET_ID=1e0709xxxx +export INFLUXDB_SCRUTINY_WEEKLY_BUCKET_ID=1af03dexxxxx +export INFLUXDB_SCRUTINY_MONTHLY_BUCKET_ID=b3c59c7xxxxx +export INFLUXDB_SCRUTINY_YEARLY_BUCKET_ID=f381d8cxxxxx + +export INFLUXDB_SCRUTINY_WEEKLY_TASK_ID=09a64ecxxxxx +export INFLUXDB_SCRUTINY_MONTHLY_TASK_ID=09a64xxxxx +export INFLUXDB_SCRUTINY_YEARLY_TASK_ID=09a64ecxxxxx + + +curl -sS -X POST ${INFLUXDB_HOSTNAME}/api/v2/authorizations \ + -H "Content-Type: application/json" \ + -H "Authorization: Token ${INFLUXDB_ADMIN_TOKEN}" \ + --data-binary @- << EOF +{ + "description": "scrutiny - restricted scope token", + "orgID": "${INFLUXDB_ORG_ID}", + "permissions": [ + { + "action": "read", + "resource": { + "type": "orgs" + } + }, + { + "action": "read", + "resource": { + "type": "tasks" + } + }, + { + "action": "write", + "resource": { + "type": "tasks", + "id": "${INFLUXDB_SCRUTINY_WEEKLY_TASK_ID}", + "orgID": "${INFLUXDB_ORG_ID}" + } + }, + { + "action": "write", + "resource": { + "type": "tasks", + "id": "${INFLUXDB_SCRUTINY_MONTHLY_TASK_ID}", + "orgID": "${INFLUXDB_ORG_ID}" + } + }, + { + "action": "write", + "resource": { + "type": "tasks", + "id": "${INFLUXDB_SCRUTINY_YEARLY_TASK_ID}", + "orgID": "${INFLUXDB_ORG_ID}" + } + }, + { + "action": "read", + "resource": { + "type": "buckets", + "id": "${INFLUXDB_SCRUTINY_BASE_BUCKET_ID}", + "orgID": "${INFLUXDB_ORG_ID}" + } + }, + { + "action": "write", + "resource": { + "type": "buckets", + "id": "${INFLUXDB_SCRUTINY_BASE_BUCKET_ID}", + "orgID": "${INFLUXDB_ORG_ID}" + } + }, + { + "action": "read", + "resource": { + "type": "buckets", + "id": "${INFLUXDB_SCRUTINY_WEEKLY_BUCKET_ID}", + "orgID": "${INFLUXDB_ORG_ID}" + } + }, + { + "action": "write", + "resource": { + "type": "buckets", + "id": "${INFLUXDB_SCRUTINY_WEEKLY_BUCKET_ID}", + "orgID": "${INFLUXDB_ORG_ID}" + } + }, + { + "action": "read", + "resource": { + "type": "buckets", + "id": "${INFLUXDB_SCRUTINY_MONTHLY_BUCKET_ID}", + "orgID": "${INFLUXDB_ORG_ID}" + } + }, + { + "action": "write", + "resource": { + "type": "buckets", + "id": "${INFLUXDB_SCRUTINY_MONTHLY_BUCKET_ID}", + "orgID": "${INFLUXDB_ORG_ID}" + } + }, + { + "action": "read", + "resource": { + "type": "buckets", + "id": "${INFLUXDB_SCRUTINY_YEARLY_BUCKET_ID}", + "orgID": "${INFLUXDB_ORG_ID}" + } + }, + { + "action": "write", + "resource": { + "type": "buckets", + "id": "${INFLUXDB_SCRUTINY_YEARLY_BUCKET_ID}", + "orgID": "${INFLUXDB_ORG_ID}" + } + } + ] +} +EOF +``` + +
+ +### Save InfluxDB API Token + +After running the Curl command above, you'll see a JSON response that looks like the following: + +```json +{ + "token": "ksVU2t5SkQwYkvIxxxxxxxYt2xUt0uRKSbSF1Po0UQ==", + "status": "active", + "description": "scrutiny - restricted scope token", + "orgID": "b2495586xxxx", + "org": "my-org", + "user": "admin", + "permissions": [ + { + "action": "read", + "resource": { + "type": "orgs" + } + }, + { + "action": "read", + "resource": { + "type": "tasks" + } + }, + { + "action": "write", + "resource": { + "type": "tasks", + "id": "09a64exxxxx", + "orgID": "b24955860xxxxx", + "org": "my-org" + } + }, + ... + ] +} +``` + +You must copy the token field from the JSON response, and save it in your `scrutiny.yaml` config file. After that's +done, you can start the Scrutiny server + diff --git a/webapp/backend/pkg/database/scrutiny_repository.go b/webapp/backend/pkg/database/scrutiny_repository.go index 28e0e2d..81f2316 100644 --- a/webapp/backend/pkg/database/scrutiny_repository.go +++ b/webapp/backend/pkg/database/scrutiny_repository.go @@ -242,21 +242,29 @@ func (sr *scrutinyRepository) EnsureBuckets(ctx context.Context, org *domain.Org //create buckets (used for downsampling) weeklyBucket := fmt.Sprintf("%s_weekly", sr.appConfig.GetString("web.influxdb.bucket")) - if _, foundErr := sr.influxClient.BucketsAPI().FindBucketByName(ctx, weeklyBucket); foundErr != nil { + if foundWeeklyBucket, foundErr := sr.influxClient.BucketsAPI().FindBucketByName(ctx, weeklyBucket); foundErr != nil { // metrics_weekly bucket will have a retention period of 8+1 weeks (since it will be down-sampled once a month) _, err := sr.influxClient.BucketsAPI().CreateBucketWithName(ctx, org, weeklyBucket, weeklyBucketRetentionRule) if err != nil { return err } + } else if sr.appConfig.GetBool("web.influxdb.retention_policy") { + //correctly set the retention period for the bucket (may not be able to do it during setup/creation) + foundWeeklyBucket.RetentionRules = domain.RetentionRules{weeklyBucketRetentionRule} + sr.influxClient.BucketsAPI().UpdateBucket(ctx, foundWeeklyBucket) } monthlyBucket := fmt.Sprintf("%s_monthly", sr.appConfig.GetString("web.influxdb.bucket")) - if _, foundErr := sr.influxClient.BucketsAPI().FindBucketByName(ctx, monthlyBucket); foundErr != nil { + if foundMonthlyBucket, foundErr := sr.influxClient.BucketsAPI().FindBucketByName(ctx, monthlyBucket); foundErr != nil { // metrics_monthly bucket will have a retention period of 24+1 months (since it will be down-sampled once a year) _, err := sr.influxClient.BucketsAPI().CreateBucketWithName(ctx, org, monthlyBucket, monthlyBucketRetentionRule) if err != nil { return err } + } else if sr.appConfig.GetBool("web.influxdb.retention_policy") { + //correctly set the retention period for the bucket (may not be able to do it during setup/creation) + foundMonthlyBucket.RetentionRules = domain.RetentionRules{monthlyBucketRetentionRule} + sr.influxClient.BucketsAPI().UpdateBucket(ctx, foundMonthlyBucket) } yearlyBucket := fmt.Sprintf("%s_yearly", sr.appConfig.GetString("web.influxdb.bucket")) From 66bd6f99c5479d6aa5dd9f8f2780d921275c5b72 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 11 Jul 2022 20:38:54 -0700 Subject: [PATCH 10/14] compiling solaris binaries related #120 --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 211a579..dfa57fb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -91,6 +91,7 @@ jobs: - { on: ubuntu-latest, goos: linux, goarch: arm, goarm: 6 } - { on: ubuntu-latest, goos: linux, goarch: arm, goarm: 7 } - { on: ubuntu-latest, goos: linux, goarch: arm64 } + - { on: ubuntu-latest, goos: solaris, goarch: amd64 } - { on: macos-latest, goos: darwin, goarch: amd64 } - { on: macos-latest, goos: darwin, goarch: arm64 } - { on: macos-latest, goos: freebsd, goarch: amd64 } From b227054b52e9052a421b33ff6f7a12628a20f032 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 11 Jul 2022 20:47:32 -0700 Subject: [PATCH 11/14] error if any step fails. --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 176d325..edc14da 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ .ONESHELL: # Applies to every targets in the file! .ONESHELL instructs make to invoke a single instance of the shell and provide it with the entire recipe, regardless of how many lines it contains. +.SHELLFLAGS = -ec ######################################################################################################################## # Global Env Settings From 64e1c93d16fbda1912e61ff541102e1061a21610 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 11 Jul 2022 20:48:30 -0700 Subject: [PATCH 12/14] add a solaris collector detect engine. --- collector/pkg/detect/devices_solaris.go | 51 +++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 collector/pkg/detect/devices_solaris.go diff --git a/collector/pkg/detect/devices_solaris.go b/collector/pkg/detect/devices_solaris.go new file mode 100644 index 0000000..ebe8e88 --- /dev/null +++ b/collector/pkg/detect/devices_solaris.go @@ -0,0 +1,51 @@ +package detect + +import ( + "github.com/analogj/scrutiny/collector/pkg/common/shell" + "github.com/analogj/scrutiny/collector/pkg/models" + "github.com/jaypipes/ghw" + "strings" +) + +func DevicePrefix() string { + return "/dev/" +} + +func (d *Detect) Start() ([]models.Device, error) { + d.Shell = shell.Create() + // call the base/common functionality to get a list of devices + detectedDevices, err := d.SmartctlScan() + if err != nil { + return nil, err + } + + //inflate device info for detected devices. + for ndx, _ := range detectedDevices { + d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors. + } + + return detectedDevices, nil +} + +//WWN values NVMe and SCSI +func (d *Detect) wwnFallback(detectedDevice *models.Device) { + block, err := ghw.Block() + if err == nil { + for _, disk := range block.Disks { + if disk.Name == detectedDevice.DeviceName && strings.ToLower(disk.WWN) != "unknown" { + d.Logger.Debugf("Found matching block device. WWN: %s", disk.WWN) + detectedDevice.WWN = disk.WWN + break + } + } + } + + //no WWN found, or could not open Block devices. Either way, fallback to serial number + if len(detectedDevice.WWN) == 0 { + d.Logger.Debugf("WWN is empty, falling back to serial number: %s", detectedDevice.SerialNumber) + detectedDevice.WWN = detectedDevice.SerialNumber + } + + //wwn must always be lowercase. + detectedDevice.WWN = strings.ToLower(detectedDevice.WWN) +} From 8e05b2e2f8ac18469738580235acf25480ae87a9 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 11 Jul 2022 20:52:15 -0700 Subject: [PATCH 13/14] Revert "add a solaris collector detect engine." This reverts commit 64e1c93d16fbda1912e61ff541102e1061a21610. https://gitlab.com/cznic/sqlite does not support Solaris. > build constraints exclude all Go files in /home/runner/work/scrutiny/scrutiny/vendor/modernc.org/libc/errno related #120 --- collector/pkg/detect/devices_solaris.go | 51 ------------------------- 1 file changed, 51 deletions(-) delete mode 100644 collector/pkg/detect/devices_solaris.go diff --git a/collector/pkg/detect/devices_solaris.go b/collector/pkg/detect/devices_solaris.go deleted file mode 100644 index ebe8e88..0000000 --- a/collector/pkg/detect/devices_solaris.go +++ /dev/null @@ -1,51 +0,0 @@ -package detect - -import ( - "github.com/analogj/scrutiny/collector/pkg/common/shell" - "github.com/analogj/scrutiny/collector/pkg/models" - "github.com/jaypipes/ghw" - "strings" -) - -func DevicePrefix() string { - return "/dev/" -} - -func (d *Detect) Start() ([]models.Device, error) { - d.Shell = shell.Create() - // call the base/common functionality to get a list of devices - detectedDevices, err := d.SmartctlScan() - if err != nil { - return nil, err - } - - //inflate device info for detected devices. - for ndx, _ := range detectedDevices { - d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors. - } - - return detectedDevices, nil -} - -//WWN values NVMe and SCSI -func (d *Detect) wwnFallback(detectedDevice *models.Device) { - block, err := ghw.Block() - if err == nil { - for _, disk := range block.Disks { - if disk.Name == detectedDevice.DeviceName && strings.ToLower(disk.WWN) != "unknown" { - d.Logger.Debugf("Found matching block device. WWN: %s", disk.WWN) - detectedDevice.WWN = disk.WWN - break - } - } - } - - //no WWN found, or could not open Block devices. Either way, fallback to serial number - if len(detectedDevice.WWN) == 0 { - d.Logger.Debugf("WWN is empty, falling back to serial number: %s", detectedDevice.SerialNumber) - detectedDevice.WWN = detectedDevice.SerialNumber - } - - //wwn must always be lowercase. - detectedDevice.WWN = strings.ToLower(detectedDevice.WWN) -} From 78410637834c0ed7758ea0a39dc8502efdf3c79e Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 11 Jul 2022 20:54:07 -0700 Subject: [PATCH 14/14] remove solaris. --- .github/workflows/ci.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dfa57fb..211a579 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -91,7 +91,6 @@ jobs: - { on: ubuntu-latest, goos: linux, goarch: arm, goarm: 6 } - { on: ubuntu-latest, goos: linux, goarch: arm, goarm: 7 } - { on: ubuntu-latest, goos: linux, goarch: arm64 } - - { on: ubuntu-latest, goos: solaris, goarch: amd64 } - { on: macos-latest, goos: darwin, goarch: amd64 } - { on: macos-latest, goos: darwin, goarch: arm64 } - { on: macos-latest, goos: freebsd, goarch: amd64 }