Merge pull request #352 from AnalogJ/beta
commit
d8d56f77f9
@ -0,0 +1,18 @@
|
||||
# Operating systems without udev
|
||||
|
||||
Some operating systems do not come with `udev` out of the box, for example Alpine Linux. In these instances you will not be able to bind `/run/udev` to the container for sharing device metadata. Some operating systems offer `udev` as a package that can be installed separately, or an alternative (such as `eudev` in the case of Alpine Linux) that provides the same functionality.
|
||||
|
||||
To install `eudev` in Alpine Linux (run as root):
|
||||
|
||||
```
|
||||
apk add eudev
|
||||
setup-udev
|
||||
```
|
||||
|
||||
Once your `udev` implementation is installed, create `/run/udev` with the following command:
|
||||
|
||||
```
|
||||
udevadm trigger
|
||||
```
|
||||
|
||||
On Alpine Linux, this also has the benefit of creating symlinks to device serial numbers in `/dev/disk/by-id`.
|
@ -1,62 +1,88 @@
|
||||
|
||||
// SQLite Table(s)
|
||||
Table device {
|
||||
created_at timestamp
|
||||
|
||||
wwn varchar [pk]
|
||||
|
||||
//user provided
|
||||
label varchar
|
||||
host_id varchar
|
||||
|
||||
// smartctl provided
|
||||
device_name varchar
|
||||
manufacturer varchar
|
||||
model_name varchar
|
||||
interface_type varchar
|
||||
interface_speed varchar
|
||||
serial_number varchar
|
||||
firmware varchar
|
||||
rotational_speed varchar
|
||||
capacity varchar
|
||||
form_factor varchar
|
||||
smart_support varchar
|
||||
device_protocol varchar
|
||||
device_type varchar
|
||||
|
||||
Table Device {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
CreatedAt time
|
||||
UpdatedAt time
|
||||
DeletedAt time
|
||||
|
||||
WWN string
|
||||
|
||||
DeviceName string
|
||||
DeviceUUID string
|
||||
DeviceSerialID string
|
||||
DeviceLabel string
|
||||
|
||||
Manufacturer string
|
||||
ModelName string
|
||||
InterfaceType string
|
||||
InterfaceSpeed string
|
||||
SerialNumber string
|
||||
Firmware string
|
||||
RotationSpeed int
|
||||
Capacity int64
|
||||
FormFactor string
|
||||
SmartSupport bool
|
||||
DeviceProtocol string//protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
|
||||
DeviceType string//device type is used for querying with -d/t flag, should only be used by collector.
|
||||
|
||||
// User provided metadata
|
||||
Label string
|
||||
HostId string
|
||||
|
||||
// Data set by Scrutiny
|
||||
DeviceStatus enum
|
||||
}
|
||||
|
||||
Table Setting {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
|
||||
// InfluxDB Tables
|
||||
Table device_temperature {
|
||||
//timestamp
|
||||
created_at timestamp
|
||||
|
||||
//tags (indexed & queryable)
|
||||
device_wwn varchar [pk]
|
||||
|
||||
//fields
|
||||
temp bigint
|
||||
}
|
||||
|
||||
SettingKeyName string
|
||||
SettingKeyDescription string
|
||||
SettingDataType string
|
||||
|
||||
Table smart_ata_results {
|
||||
//timestamp
|
||||
created_at timestamp
|
||||
|
||||
//tags (indexed & queryable)
|
||||
device_wwn varchar [pk]
|
||||
smart_status varchar
|
||||
scrutiny_status varchar
|
||||
SettingValueNumeric int64
|
||||
SettingValueString string
|
||||
}
|
||||
|
||||
|
||||
// InfluxDB Tables
|
||||
Table SmartTemperature {
|
||||
Date time
|
||||
DeviceWWN string //(tag)
|
||||
Temp int64
|
||||
}
|
||||
|
||||
//fields
|
||||
temp bigint
|
||||
power_on_hours bigint
|
||||
power_cycle_count bigint
|
||||
|
||||
Table Smart {
|
||||
Date time
|
||||
DeviceWWN string //(tag)
|
||||
DeviceProtocol string
|
||||
|
||||
//Metrics (fields)
|
||||
Temp int64
|
||||
PowerOnHours int64
|
||||
PowerCycleCount int64
|
||||
|
||||
//Smart Status
|
||||
Status enum
|
||||
|
||||
//SMART Attributes (fields)
|
||||
Attr_ID_AttributeId int
|
||||
Attr_ID_Value int64
|
||||
Attr_ID_Threshold int64
|
||||
Attr_ID_Worst int64
|
||||
Attr_ID_RawValue int64
|
||||
Attr_ID_RawString string
|
||||
Attr_ID_WhenFailed string
|
||||
//Generated data
|
||||
Attr_ID_TransformedValue int64
|
||||
Attr_ID_Status enum
|
||||
Attr_ID_StatusReason string
|
||||
Attr_ID_FailureRate float64
|
||||
|
||||
}
|
||||
|
||||
Ref: device.wwn < smart_ata_results.device_wwn
|
||||
Ref: Device.WWN < Smart.DeviceWWN
|
||||
Ref: Device.WWN < SmartTemperature.DeviceWWN
|
||||
|
@ -0,0 +1,34 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_MergeConfigMap(t *testing.T) {
|
||||
//setup
|
||||
testConfig := configuration{
|
||||
Viper: viper.New(),
|
||||
}
|
||||
testConfig.Set("user.dashboard_display", "hello")
|
||||
testConfig.SetDefault("user.layout", "hello")
|
||||
|
||||
mergeSettings := map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"dashboard_display": "dashboard_display",
|
||||
"layout": "layout",
|
||||
},
|
||||
}
|
||||
//test
|
||||
err := testConfig.MergeConfigMap(mergeSettings)
|
||||
|
||||
//verify
|
||||
require.NoError(t, err)
|
||||
|
||||
// if using Set, the MergeConfigMap functionality will not override
|
||||
// if using SetDefault, the MergeConfigMap will override correctly
|
||||
require.Equal(t, "hello", testConfig.GetString("user.dashboard_display"))
|
||||
require.Equal(t, "layout", testConfig.GetString("user.layout"))
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package m20220716214900
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
gorm.Model
|
||||
|
||||
SettingKeyName string `json:"setting_key_name"`
|
||||
SettingKeyDescription string `json:"setting_key_description"`
|
||||
SettingDataType string `json:"setting_data_type"`
|
||||
|
||||
SettingValueNumeric int `json:"setting_value_numeric"`
|
||||
SettingValueString string `json:"setting_value_string"`
|
||||
SettingValueBool bool `json:"setting_value_bool"`
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LoadSettings will retrieve settings from the database, store them in the AppConfig object, and return a Settings struct
|
||||
func (sr *scrutinyRepository) LoadSettings(ctx context.Context) (*models.Settings, error) {
|
||||
settingsEntries := []models.SettingEntry{}
|
||||
if err := sr.gormClient.WithContext(ctx).Find(&settingsEntries).Error; err != nil {
|
||||
return nil, fmt.Errorf("Could not get settings from DB: %v", err)
|
||||
}
|
||||
|
||||
// store retrieved settings in the AppConfig obj
|
||||
for _, settingsEntry := range settingsEntries {
|
||||
configKey := fmt.Sprintf("%s.%s", config.DB_USER_SETTINGS_SUBKEY, settingsEntry.SettingKeyName)
|
||||
|
||||
if settingsEntry.SettingDataType == "numeric" {
|
||||
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueNumeric)
|
||||
} else if settingsEntry.SettingDataType == "string" {
|
||||
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueString)
|
||||
} else if settingsEntry.SettingDataType == "bool" {
|
||||
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueBool)
|
||||
}
|
||||
}
|
||||
|
||||
// unmarshal the dbsetting object data to a settings object.
|
||||
var settings models.Settings
|
||||
err := sr.appConfig.UnmarshalKey(config.DB_USER_SETTINGS_SUBKEY, &settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// testing
|
||||
// curl -d '{"metrics": { "notify_level": 5, "status_filter_attributes": 5, "status_threshold": 5 }}' -H "Content-Type: application/json" -X POST http://localhost:9090/api/settings
|
||||
// SaveSettings will update settings in AppConfig object, then save the settings to the database.
|
||||
func (sr *scrutinyRepository) SaveSettings(ctx context.Context, settings models.Settings) error {
|
||||
//save the entries to the appconfig
|
||||
settingsMap := &map[string]interface{}{}
|
||||
err := mapstructure.Decode(settings, &settingsMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settingsWrapperMap := map[string]interface{}{}
|
||||
settingsWrapperMap[config.DB_USER_SETTINGS_SUBKEY] = *settingsMap
|
||||
err = sr.appConfig.MergeConfigMap(settingsWrapperMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sr.logger.Debugf("after merge settings: %v", sr.appConfig.AllSettings())
|
||||
//retrieve current settings from the database
|
||||
settingsEntries := []models.SettingEntry{}
|
||||
if err := sr.gormClient.WithContext(ctx).Find(&settingsEntries).Error; err != nil {
|
||||
return fmt.Errorf("Could not get settings from DB: %v", err)
|
||||
}
|
||||
|
||||
//update settingsEntries
|
||||
for ndx, settingsEntry := range settingsEntries {
|
||||
configKey := fmt.Sprintf("%s.%s", config.DB_USER_SETTINGS_SUBKEY, strings.ToLower(settingsEntry.SettingKeyName))
|
||||
|
||||
if settingsEntry.SettingDataType == "numeric" {
|
||||
settingsEntries[ndx].SettingValueNumeric = sr.appConfig.GetInt(configKey)
|
||||
} else if settingsEntry.SettingDataType == "string" {
|
||||
settingsEntries[ndx].SettingValueString = sr.appConfig.GetString(configKey)
|
||||
} else if settingsEntry.SettingDataType == "bool" {
|
||||
settingsEntries[ndx].SettingValueBool = sr.appConfig.GetBool(configKey)
|
||||
}
|
||||
|
||||
// store in database.
|
||||
//TODO: this should be `sr.gormClient.Updates(&settingsEntries).Error`
|
||||
err := sr.gormClient.Model(&models.SettingEntry{}).Where([]uint{settingsEntry.ID}).Select("setting_value_numeric", "setting_value_string", "setting_value_bool").Updates(settingsEntries[ndx]).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package models
|
||||
|
||||
// Temperature Format
|
||||
// Date Format
|
||||
// Device History window
|
@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SettingEntry matches a setting row in the database
|
||||
type SettingEntry struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
gorm.Model
|
||||
|
||||
SettingKeyName string `json:"setting_key_name" gorm:"unique;not null"`
|
||||
SettingKeyDescription string `json:"setting_key_description"`
|
||||
SettingDataType string `json:"setting_data_type"`
|
||||
|
||||
SettingValueNumeric int `json:"setting_value_numeric"`
|
||||
SettingValueString string `json:"setting_value_string"`
|
||||
SettingValueBool bool `json:"setting_value_bool"`
|
||||
}
|
||||
|
||||
func (s SettingEntry) TableName() string {
|
||||
return "settings"
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
// Settings is made up of parsed SettingEntry objects retrieved from the database
|
||||
//type Settings struct {
|
||||
// MetricsNotifyLevel pkg.MetricsNotifyLevel `json:"metrics.notify.level" mapstructure:"metrics.notify.level"`
|
||||
// MetricsStatusFilterAttributes pkg.MetricsStatusFilterAttributes `json:"metrics.status.filter_attributes" mapstructure:"metrics.status.filter_attributes"`
|
||||
// MetricsStatusThreshold pkg.MetricsStatusThreshold `json:"metrics.status.threshold" mapstructure:"metrics.status.threshold"`
|
||||
//}
|
||||
|
||||
type Settings struct {
|
||||
Theme string `json:"theme" mapstructure:"theme"`
|
||||
Layout string `json:"layout" mapstructure:"layout"`
|
||||
DashboardDisplay string `json:"dashboard_display" mapstructure:"dashboard_display"`
|
||||
DashboardSort string `json:"dashboard_sort" mapstructure:"dashboard_sort"`
|
||||
TemperatureUnit string `json:"temperature_unit" mapstructure:"temperature_unit"`
|
||||
FileSizeSIUnits bool `json:"file_size_si_units" mapstructure:"file_size_si_units"`
|
||||
|
||||
Metrics struct {
|
||||
NotifyLevel int `json:"notify_level" mapstructure:"notify_level"`
|
||||
StatusFilterAttributes int `json:"status_filter_attributes" mapstructure:"status_filter_attributes"`
|
||||
StatusThreshold int `json:"status_threshold" mapstructure:"status_threshold"`
|
||||
} `json:"metrics" mapstructure:"metrics"`
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GetSettings(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
settings, err := deviceRepo.LoadSettings(c)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while retrieving settings", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"settings": settings,
|
||||
})
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func SaveSettings(c *gin.Context) {
|
||||
logger := c.MustGet("LOGGER").(*logrus.Entry)
|
||||
deviceRepo := c.MustGet("DEVICE_REPOSITORY").(database.DeviceRepo)
|
||||
|
||||
var settings models.Settings
|
||||
err := c.BindJSON(&settings)
|
||||
if err != nil {
|
||||
logger.Errorln("Cannot parse updated settings", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
err = deviceRepo.SaveSettings(c, settings)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while saving settings", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"settings": settings,
|
||||
})
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import {ModuleWithProviders, NgModule} from '@angular/core';
|
||||
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
|
||||
import {TREO_APP_CONFIG} from '@treo/services/config/config.constants';
|
||||
|
||||
@NgModule()
|
||||
export class ScrutinyConfigModule {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {ScrutinyConfigService} _scrutinyConfigService
|
||||
*/
|
||||
constructor(
|
||||
private _scrutinyConfigService: ScrutinyConfigService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* forRoot method for setting user configuration
|
||||
*
|
||||
* @param config
|
||||
*/
|
||||
static forRoot(config: any): ModuleWithProviders {
|
||||
return {
|
||||
ngModule: ScrutinyConfigModule,
|
||||
providers: [
|
||||
{
|
||||
provide: TREO_APP_CONFIG,
|
||||
useValue: config
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
import {Inject, Injectable} from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {TREO_APP_CONFIG} from '@treo/services/config/config.constants';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
import {getBasePath} from '../../app.routing';
|
||||
import {map, tap} from 'rxjs/operators';
|
||||
import {AppConfig} from './app.config';
|
||||
import {merge} from 'lodash';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ScrutinyConfigService {
|
||||
// Private
|
||||
private _config: BehaviorSubject<AppConfig>;
|
||||
private _defaultConfig: AppConfig;
|
||||
|
||||
constructor(
|
||||
private _httpClient: HttpClient,
|
||||
@Inject(TREO_APP_CONFIG) defaultConfig: AppConfig
|
||||
) {
|
||||
// Set the private defaults
|
||||
this._defaultConfig = defaultConfig
|
||||
this._config = new BehaviorSubject(null);
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Setter & getter for config
|
||||
*/
|
||||
set config(value: AppConfig) {
|
||||
// get the current config, merge the new values, and then submit. (setTheme only sets a single key, not the whole obj)
|
||||
const mergedSettings = merge({}, this._config.getValue(), value);
|
||||
|
||||
console.log('saving settings...', mergedSettings)
|
||||
this._httpClient.post(getBasePath() + '/api/settings', mergedSettings).pipe(
|
||||
map((response: any) => {
|
||||
console.log('settings resp')
|
||||
return response.settings
|
||||
}),
|
||||
tap((settings: AppConfig) => {
|
||||
this._config.next(settings);
|
||||
return settings
|
||||
})
|
||||
).subscribe(resp => {
|
||||
console.log('updated settings', resp)
|
||||
})
|
||||
}
|
||||
|
||||
get config$(): Observable<AppConfig> {
|
||||
if (this._config.getValue()) {
|
||||
console.log('using cached settings:', this._config.getValue())
|
||||
return this._config.asObservable()
|
||||
} else {
|
||||
console.log('retrieving settings')
|
||||
return this._httpClient.get(getBasePath() + '/api/settings').pipe(
|
||||
map((response: any) => {
|
||||
return response.settings
|
||||
}),
|
||||
tap((settings: AppConfig) => {
|
||||
this._config.next(settings);
|
||||
return this._config.asObservable()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resets the config to the default
|
||||
*/
|
||||
reset(): void {
|
||||
// Set the config
|
||||
this.config = this._defaultConfig
|
||||
}
|
||||
}
|
@ -1,8 +1,146 @@
|
||||
import { DeviceStatusPipe } from './device-status.pipe';
|
||||
import {DeviceStatusPipe} from './device-status.pipe';
|
||||
import {MetricsStatusThreshold} from '../core/config/app.config';
|
||||
import {DeviceModel} from '../core/models/device-model';
|
||||
|
||||
describe('DeviceStatusPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new DeviceStatusPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
it('create an instance', () => {
|
||||
const pipe = new DeviceStatusPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('#deviceStatusForModelWithThreshold', () => {
|
||||
it('if healthy device, should be passing', () => {
|
||||
expect(DeviceStatusPipe.deviceStatusForModelWithThreshold(
|
||||
{device_status: 0} as DeviceModel,
|
||||
true,
|
||||
MetricsStatusThreshold.Both
|
||||
)).toBe('passed')
|
||||
});
|
||||
|
||||
it('if device with no smart data, should be unknown', () => {
|
||||
expect(DeviceStatusPipe.deviceStatusForModelWithThreshold(
|
||||
{device_status: 0} as DeviceModel,
|
||||
false,
|
||||
MetricsStatusThreshold.Both
|
||||
)).toBe('unknown')
|
||||
});
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
'deviceStatus': 10000, // invalid status
|
||||
'hasSmartResults': false,
|
||||
'threshold': MetricsStatusThreshold.Smart,
|
||||
'includeReason': false,
|
||||
'result': 'unknown'
|
||||
},
|
||||
|
||||
{
|
||||
'deviceStatus': 1,
|
||||
'hasSmartResults': true,
|
||||
'threshold': MetricsStatusThreshold.Smart,
|
||||
'includeReason': false,
|
||||
'result': 'failed'
|
||||
},
|
||||
{
|
||||
'deviceStatus': 1,
|
||||
'hasSmartResults': true,
|
||||
'threshold': MetricsStatusThreshold.Scrutiny,
|
||||
'includeReason': false,
|
||||
'result': 'passed'
|
||||
},
|
||||
{
|
||||
'deviceStatus': 1,
|
||||
'hasSmartResults': true,
|
||||
'threshold': MetricsStatusThreshold.Both,
|
||||
'includeReason': false,
|
||||
'result': 'failed'
|
||||
},
|
||||
|
||||
{
|
||||
'deviceStatus': 2,
|
||||
'hasSmartResults': true,
|
||||
'threshold': MetricsStatusThreshold.Smart,
|
||||
'includeReason': false,
|
||||
'result': 'passed'
|
||||
},
|
||||
{
|
||||
'deviceStatus': 2,
|
||||
'hasSmartResults': true,
|
||||
'threshold': MetricsStatusThreshold.Scrutiny,
|
||||
'includeReason': false,
|
||||
'result': 'failed'
|
||||
},
|
||||
{
|
||||
'deviceStatus': 2,
|
||||
'hasSmartResults': true,
|
||||
'threshold': MetricsStatusThreshold.Both,
|
||||
'includeReason': false,
|
||||
'result': 'failed'
|
||||
},
|
||||
|
||||
{
|
||||
'deviceStatus': 3,
|
||||
'hasSmartResults': true,
|
||||
'threshold': MetricsStatusThreshold.Smart,
|
||||
'includeReason': false,
|
||||
'result': 'failed'
|
||||
},
|
||||
{
|
||||
'deviceStatus': 3,
|
||||
'hasSmartResults': true,
|
||||
'threshold': MetricsStatusThreshold.Scrutiny,
|
||||
'includeReason': false,
|
||||
'result': 'failed'
|
||||
},
|
||||
{
|
||||
'deviceStatus': 3,
|
||||
'hasSmartResults': true,
|
||||
'threshold': MetricsStatusThreshold.Both,
|
||||
'includeReason': false,
|
||||
'result': 'failed'
|
||||
},
|
||||
|
||||
{
|
||||
'deviceStatus': 3,
|
||||
'hasSmartResults': false,
|
||||
'threshold': MetricsStatusThreshold.Smart,
|
||||
'includeReason': true,
|
||||
'result': 'unknown'
|
||||
},
|
||||
{
|
||||
'deviceStatus': 3,
|
||||
'hasSmartResults': true,
|
||||
'threshold': MetricsStatusThreshold.Smart,
|
||||
'includeReason': true,
|
||||
'result': 'failed: smart'
|
||||
},
|
||||
{
|
||||
'deviceStatus': 3,
|
||||
'hasSmartResults': true,
|
||||
'threshold': MetricsStatusThreshold.Scrutiny,
|
||||
'includeReason': true,
|
||||
'result': 'failed: scrutiny'
|
||||
},
|
||||
{
|
||||
'deviceStatus': 3,
|
||||
'hasSmartResults': true,
|
||||
'threshold': MetricsStatusThreshold.Both,
|
||||
'includeReason': true,
|
||||
'result': 'failed: both'
|
||||
}
|
||||
|
||||
|
||||
]
|
||||
|
||||
testCases.forEach((test, index) => {
|
||||
it(`if device with status (${test.deviceStatus}), hasSmartResults(${test.hasSmartResults}) and threshold (${test.threshold}), should be ${test.result}`, () => {
|
||||
expect(DeviceStatusPipe.deviceStatusForModelWithThreshold(
|
||||
{device_status: test.deviceStatus} as DeviceModel,
|
||||
test.hasSmartResults,
|
||||
test.threshold,
|
||||
test.includeReason
|
||||
)).toBe(test.result)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,21 +1,71 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {Pipe, PipeTransform} from '@angular/core';
|
||||
import {MetricsStatusThreshold} from '../core/config/app.config';
|
||||
import {DeviceModel} from '../core/models/device-model';
|
||||
|
||||
const DEVICE_STATUS_NAMES: { [key: number]: string } = {
|
||||
0: 'passed',
|
||||
1: 'failed',
|
||||
2: 'failed',
|
||||
3: 'failed'
|
||||
};
|
||||
|
||||
const DEVICE_STATUS_NAMES_WITH_REASON: { [key: number]: string } = {
|
||||
0: 'passed',
|
||||
1: 'failed: smart',
|
||||
2: 'failed: scrutiny',
|
||||
3: 'failed: both'
|
||||
};
|
||||
|
||||
|
||||
@Pipe({
|
||||
name: 'deviceStatus'
|
||||
name: 'deviceStatus'
|
||||
})
|
||||
export class DeviceStatusPipe implements PipeTransform {
|
||||
|
||||
transform(deviceStatusFlag: number): string {
|
||||
if(deviceStatusFlag === 0){
|
||||
return 'passed'
|
||||
} else if(deviceStatusFlag === 3){
|
||||
return 'failed: both'
|
||||
} else if(deviceStatusFlag === 2) {
|
||||
return 'failed: scrutiny'
|
||||
} else if(deviceStatusFlag === 1) {
|
||||
return 'failed: smart'
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
static deviceStatusForModelWithThreshold(
|
||||
deviceModel: DeviceModel,
|
||||
hasSmartResults: boolean = true,
|
||||
threshold: MetricsStatusThreshold = MetricsStatusThreshold.Both,
|
||||
includeReason: boolean = false
|
||||
): string {
|
||||
// no smart data, so treat the device status as unknown
|
||||
if (!hasSmartResults) {
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
let statusNameLookup = DEVICE_STATUS_NAMES
|
||||
if (includeReason) {
|
||||
statusNameLookup = DEVICE_STATUS_NAMES_WITH_REASON
|
||||
}
|
||||
// determine the device status, by comparing it against the allowed threshold
|
||||
// tslint:disable-next-line:no-bitwise
|
||||
const deviceStatus = deviceModel.device_status & threshold
|
||||
return statusNameLookup[deviceStatus]
|
||||
}
|
||||
|
||||
// static deviceStatusForModelWithThreshold(deviceModel: DeviceModel | any, threshold: MetricsStatusThreshold): string {
|
||||
// // tslint:disable-next-line:no-bitwise
|
||||
// const deviceStatus = deviceModel?.device_status & threshold
|
||||
// if(deviceStatus === 0){
|
||||
// return 'passed'
|
||||
// } else if(deviceStatus === 3){
|
||||
// return 'failed: both'
|
||||
// } else if(deviceStatus === 2) {
|
||||
// return 'failed: scrutiny'
|
||||
// } else if(deviceStatus === 1) {
|
||||
// return 'failed: smart'
|
||||
// }
|
||||
// return 'unknown'
|
||||
// }
|
||||
|
||||
transform(
|
||||
deviceModel: DeviceModel,
|
||||
hasSmartResults: boolean = true,
|
||||
threshold: MetricsStatusThreshold = MetricsStatusThreshold.Both,
|
||||
includeReason: boolean = false
|
||||
): string {
|
||||
return DeviceStatusPipe.deviceStatusForModelWithThreshold(deviceModel, hasSmartResults, threshold, includeReason)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,75 +1,27 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright (c) 2019 Jonathan Catmull.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {Pipe, PipeTransform} from '@angular/core';
|
||||
|
||||
type unit = 'bytes' | 'KB' | 'MB' | 'GB' | 'TB' | 'PB';
|
||||
type unitPrecisionMap = {
|
||||
[u in unit]: number;
|
||||
};
|
||||
|
||||
const defaultPrecisionMap: unitPrecisionMap = {
|
||||
bytes: 0,
|
||||
KB: 0,
|
||||
MB: 1,
|
||||
GB: 1,
|
||||
TB: 2,
|
||||
PB: 2
|
||||
};
|
||||
|
||||
/*
|
||||
* Convert bytes into largest possible unit.
|
||||
* Takes an precision argument that can be a number or a map for each unit.
|
||||
* Usage:
|
||||
* bytes | fileSize:precision
|
||||
* @example
|
||||
* // returns 1 KB
|
||||
* {{ 1500 | fileSize }}
|
||||
* @example
|
||||
* // returns 2.1 GB
|
||||
* {{ 2100000000 | fileSize }}
|
||||
* @example
|
||||
* // returns 1.46 KB
|
||||
* {{ 1500 | fileSize:2 }}
|
||||
*/
|
||||
@Pipe({ name: 'fileSize' })
|
||||
@Pipe({name: 'fileSize'})
|
||||
export class FileSizePipe implements PipeTransform {
|
||||
private readonly units: unit[] = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
|
||||
transform(bytes: number = 0, precision: number | unitPrecisionMap = defaultPrecisionMap): string {
|
||||
if (isNaN(parseFloat(String(bytes))) || !isFinite(bytes)) return '?';
|
||||
|
||||
let unitIndex = 0;
|
||||
transform(bytes: number = 0, si = false, dp = 1): string {
|
||||
const thresh = si ? 1000 : 1024;
|
||||
|
||||
while (bytes >= 1024) {
|
||||
bytes /= 1024;
|
||||
unitIndex++;
|
||||
if (Math.abs(bytes) < thresh) {
|
||||
return bytes + ' B';
|
||||
}
|
||||
|
||||
const unit = this.units[unitIndex];
|
||||
const units = si
|
||||
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
let u = -1;
|
||||
const r = 10 ** dp;
|
||||
|
||||
if (typeof precision === 'number') {
|
||||
return `${bytes.toFixed(+precision)} ${unit}`;
|
||||
}
|
||||
return `${bytes.toFixed(precision[unit])} ${unit}`;
|
||||
do {
|
||||
bytes /= thresh;
|
||||
++u;
|
||||
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
|
||||
|
||||
|
||||
return bytes.toFixed(dp) + ' ' + units[u];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in new issue