Merge pull request #338 from AnalogJ/app_db_settings
This commit is contained in:
+70
-44
@@ -1,62 +1,88 @@
|
|||||||
|
|
||||||
// SQLite Table(s)
|
// SQLite Table(s)
|
||||||
Table device {
|
|
||||||
created_at timestamp
|
|
||||||
|
|
||||||
wwn varchar [pk]
|
Table Device {
|
||||||
|
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||||
|
CreatedAt time
|
||||||
|
UpdatedAt time
|
||||||
|
DeletedAt time
|
||||||
|
|
||||||
//user provided
|
WWN string
|
||||||
label varchar
|
|
||||||
host_id varchar
|
|
||||||
|
|
||||||
// smartctl provided
|
DeviceName string
|
||||||
device_name varchar
|
DeviceUUID string
|
||||||
manufacturer varchar
|
DeviceSerialID string
|
||||||
model_name varchar
|
DeviceLabel string
|
||||||
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
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
SettingKeyName string
|
||||||
|
SettingKeyDescription string
|
||||||
|
SettingDataType string
|
||||||
|
|
||||||
|
SettingValueNumeric int64
|
||||||
|
SettingValueString string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// InfluxDB Tables
|
// InfluxDB Tables
|
||||||
Table device_temperature {
|
Table SmartTemperature {
|
||||||
//timestamp
|
Date time
|
||||||
created_at timestamp
|
DeviceWWN string //(tag)
|
||||||
|
Temp int64
|
||||||
//tags (indexed & queryable)
|
}
|
||||||
device_wwn varchar [pk]
|
|
||||||
|
|
||||||
//fields
|
|
||||||
temp bigint
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Table smart_ata_results {
|
Table Smart {
|
||||||
//timestamp
|
Date time
|
||||||
created_at timestamp
|
DeviceWWN string //(tag)
|
||||||
|
DeviceProtocol string
|
||||||
|
|
||||||
//tags (indexed & queryable)
|
//Metrics (fields)
|
||||||
device_wwn varchar [pk]
|
Temp int64
|
||||||
smart_status varchar
|
PowerOnHours int64
|
||||||
scrutiny_status varchar
|
PowerCycleCount int64
|
||||||
|
|
||||||
|
//Smart Status
|
||||||
|
Status enum
|
||||||
|
|
||||||
|
//SMART Attributes (fields)
|
||||||
//fields
|
Attr_ID_AttributeId int
|
||||||
temp bigint
|
Attr_ID_Value int64
|
||||||
power_on_hours bigint
|
Attr_ID_Threshold int64
|
||||||
power_cycle_count bigint
|
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
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/analogj/go-util/utils"
|
"github.com/analogj/go-util/utils"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"log"
|
"log"
|
||||||
@@ -10,6 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DB_USER_SETTINGS_SUBKEY = "user"
|
||||||
|
|
||||||
// When initializing this class the following methods must be called:
|
// When initializing this class the following methods must be called:
|
||||||
// Config.New
|
// Config.New
|
||||||
// Config.Init
|
// Config.Init
|
||||||
@@ -39,8 +40,6 @@ func (c *configuration) Init() error {
|
|||||||
c.SetDefault("log.file", "")
|
c.SetDefault("log.file", "")
|
||||||
|
|
||||||
c.SetDefault("notify.urls", []string{})
|
c.SetDefault("notify.urls", []string{})
|
||||||
c.SetDefault("notify.filter_attributes", pkg.NotifyFilterAttributesAll)
|
|
||||||
c.SetDefault("notify.level", pkg.NotifyLevelFail)
|
|
||||||
|
|
||||||
c.SetDefault("web.influxdb.scheme", "http")
|
c.SetDefault("web.influxdb.scheme", "http")
|
||||||
c.SetDefault("web.influxdb.host", "localhost")
|
c.SetDefault("web.influxdb.host", "localhost")
|
||||||
@@ -55,17 +54,6 @@ func (c *configuration) Init() error {
|
|||||||
//c.SetDefault("disks.include", []string{})
|
//c.SetDefault("disks.include", []string{})
|
||||||
//c.SetDefault("disks.exclude", []string{})
|
//c.SetDefault("disks.exclude", []string{})
|
||||||
|
|
||||||
//c.SetDefault("notify.metric.script", "/opt/scrutiny/config/notify-metrics.sh")
|
|
||||||
//c.SetDefault("notify.long.script", "/opt/scrutiny/config/notify-long-test.sh")
|
|
||||||
//c.SetDefault("notify.short.script", "/opt/scrutiny/config/notify-short-test.sh")
|
|
||||||
|
|
||||||
//c.SetDefault("collect.metric.enable", true)
|
|
||||||
//c.SetDefault("collect.metric.command", "-a -o on -S on")
|
|
||||||
//c.SetDefault("collect.long.enable", true)
|
|
||||||
//c.SetDefault("collect.long.command", "-a -o on -S on")
|
|
||||||
//c.SetDefault("collect.short.enable", true)
|
|
||||||
//c.SetDefault("collect.short.command", "-a -o on -S on")
|
|
||||||
|
|
||||||
//if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
|
//if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
|
||||||
c.SetConfigType("yaml")
|
c.SetConfigType("yaml")
|
||||||
//c.SetConfigName("drawbridge")
|
//c.SetConfigName("drawbridge")
|
||||||
@@ -77,7 +65,18 @@ func (c *configuration) Init() error {
|
|||||||
c.AutomaticEnv()
|
c.AutomaticEnv()
|
||||||
|
|
||||||
//CLI options will be added via the `Set()` function
|
//CLI options will be added via the `Set()` function
|
||||||
return nil
|
return c.ValidateConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configuration) SubKeys(key string) []string {
|
||||||
|
return c.Sub(key).AllKeys()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configuration) Sub(key string) Interface {
|
||||||
|
config := configuration{
|
||||||
|
Viper: c.Viper.Sub(key),
|
||||||
|
}
|
||||||
|
return &config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *configuration) ReadConfig(configFilePath string) error {
|
func (c *configuration) ReadConfig(configFilePath string) error {
|
||||||
@@ -120,24 +119,18 @@ func (c *configuration) ReadConfig(configFilePath string) error {
|
|||||||
// This function ensures that the merged config works correctly.
|
// This function ensures that the merged config works correctly.
|
||||||
func (c *configuration) ValidateConfig() error {
|
func (c *configuration) ValidateConfig() error {
|
||||||
|
|
||||||
////deserialize Questions
|
//the following keys are deprecated, and no longer supported
|
||||||
//questionsMap := map[string]Question{}
|
/*
|
||||||
//err := c.UnmarshalKey("questions", &questionsMap)
|
- notify.filter_attributes (replaced by metrics.status.filter_attributes SETTING)
|
||||||
//
|
- notify.level (replaced by metrics.notify.level and metrics.status.threshold SETTING)
|
||||||
//if err != nil {
|
*/
|
||||||
// log.Printf("questions could not be deserialized correctly. %v", err)
|
//TODO add docs and upgrade doc.
|
||||||
// return err
|
if c.IsSet("notify.filter_attributes") {
|
||||||
//}
|
return errors.ConfigValidationError("`notify.filter_attributes` configuration option is deprecated. Replaced by option in Dashboard Settings page")
|
||||||
//
|
}
|
||||||
//for _, v := range questionsMap {
|
if c.IsSet("notify.level") {
|
||||||
//
|
return errors.ConfigValidationError("`notify.level` configuration option is deprecated. Replaced by option in Dashboard Settings page")
|
||||||
// typeContent, ok := v.Schema["type"].(string)
|
}
|
||||||
// if !ok || len(typeContent) == 0 {
|
|
||||||
// return errors.QuestionSyntaxError("`type` is required for questions")
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|
||||||
|
}
|
||||||
@@ -12,12 +12,17 @@ type Interface interface {
|
|||||||
WriteConfig() error
|
WriteConfig() error
|
||||||
Set(key string, value interface{})
|
Set(key string, value interface{})
|
||||||
SetDefault(key string, value interface{})
|
SetDefault(key string, value interface{})
|
||||||
|
MergeConfigMap(cfg map[string]interface{}) error
|
||||||
|
|
||||||
|
Sub(key string) Interface
|
||||||
AllSettings() map[string]interface{}
|
AllSettings() map[string]interface{}
|
||||||
|
AllKeys() []string
|
||||||
|
SubKeys(key string) []string
|
||||||
IsSet(key string) bool
|
IsSet(key string) bool
|
||||||
Get(key string) interface{}
|
Get(key string) interface{}
|
||||||
GetBool(key string) bool
|
GetBool(key string) bool
|
||||||
GetInt(key string) int
|
GetInt(key string) int
|
||||||
|
GetInt64(key string) int64
|
||||||
GetString(key string) string
|
GetString(key string) string
|
||||||
GetStringSlice(key string) []string
|
GetStringSlice(key string) []string
|
||||||
UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error
|
UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package mock_config
|
|||||||
import (
|
import (
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
|
|
||||||
|
config "github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
viper "github.com/spf13/viper"
|
viper "github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
@@ -34,6 +35,20 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
|||||||
return m.recorder
|
return m.recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AllKeys mocks base method.
|
||||||
|
func (m *MockInterface) AllKeys() []string {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "AllKeys")
|
||||||
|
ret0, _ := ret[0].([]string)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllKeys indicates an expected call of AllKeys.
|
||||||
|
func (mr *MockInterfaceMockRecorder) AllKeys() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllKeys", reflect.TypeOf((*MockInterface)(nil).AllKeys))
|
||||||
|
}
|
||||||
|
|
||||||
// AllSettings mocks base method.
|
// AllSettings mocks base method.
|
||||||
func (m *MockInterface) AllSettings() map[string]interface{} {
|
func (m *MockInterface) AllSettings() map[string]interface{} {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -90,6 +105,20 @@ func (mr *MockInterfaceMockRecorder) GetInt(key interface{}) *gomock.Call {
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockInterface)(nil).GetInt), key)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockInterface)(nil).GetInt), key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInt64 mocks base method.
|
||||||
|
func (m *MockInterface) GetInt64(key string) int64 {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetInt64", key)
|
||||||
|
ret0, _ := ret[0].(int64)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInt64 indicates an expected call of GetInt64.
|
||||||
|
func (mr *MockInterfaceMockRecorder) GetInt64(key interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt64", reflect.TypeOf((*MockInterface)(nil).GetInt64), key)
|
||||||
|
}
|
||||||
|
|
||||||
// GetString mocks base method.
|
// GetString mocks base method.
|
||||||
func (m *MockInterface) GetString(key string) string {
|
func (m *MockInterface) GetString(key string) string {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -146,6 +175,20 @@ func (mr *MockInterfaceMockRecorder) IsSet(key interface{}) *gomock.Call {
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSet", reflect.TypeOf((*MockInterface)(nil).IsSet), key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MergeConfigMap mocks base method.
|
||||||
|
func (m *MockInterface) MergeConfigMap(cfg map[string]interface{}) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "MergeConfigMap", cfg)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeConfigMap indicates an expected call of MergeConfigMap.
|
||||||
|
func (mr *MockInterfaceMockRecorder) MergeConfigMap(cfg interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MergeConfigMap", reflect.TypeOf((*MockInterface)(nil).MergeConfigMap), cfg)
|
||||||
|
}
|
||||||
|
|
||||||
// ReadConfig mocks base method.
|
// ReadConfig mocks base method.
|
||||||
func (m *MockInterface) ReadConfig(configFilePath string) error {
|
func (m *MockInterface) ReadConfig(configFilePath string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -184,6 +227,34 @@ func (mr *MockInterfaceMockRecorder) SetDefault(key, value interface{}) *gomock.
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefault", reflect.TypeOf((*MockInterface)(nil).SetDefault), key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sub mocks base method.
|
||||||
|
func (m *MockInterface) Sub(key string) config.Interface {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Sub", key)
|
||||||
|
ret0, _ := ret[0].(config.Interface)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sub indicates an expected call of Sub.
|
||||||
|
func (mr *MockInterfaceMockRecorder) Sub(key interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sub", reflect.TypeOf((*MockInterface)(nil).Sub), key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubKeys mocks base method.
|
||||||
|
func (m *MockInterface) SubKeys(key string) []string {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "SubKeys", key)
|
||||||
|
ret0, _ := ret[0].([]string)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubKeys indicates an expected call of SubKeys.
|
||||||
|
func (mr *MockInterfaceMockRecorder) SubKeys(key interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubKeys", reflect.TypeOf((*MockInterface)(nil).SubKeys), key)
|
||||||
|
}
|
||||||
|
|
||||||
// UnmarshalKey mocks base method.
|
// UnmarshalKey mocks base method.
|
||||||
func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error {
|
func (m *MockInterface) UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|||||||
@@ -4,17 +4,11 @@ const DeviceProtocolAta = "ATA"
|
|||||||
const DeviceProtocolScsi = "SCSI"
|
const DeviceProtocolScsi = "SCSI"
|
||||||
const DeviceProtocolNvme = "NVMe"
|
const DeviceProtocolNvme = "NVMe"
|
||||||
|
|
||||||
const NotifyFilterAttributesAll = "all"
|
|
||||||
const NotifyFilterAttributesCritical = "critical"
|
|
||||||
|
|
||||||
const NotifyLevelFail = "fail"
|
|
||||||
const NotifyLevelFailScrutiny = "fail_scrutiny"
|
|
||||||
const NotifyLevelFailSmart = "fail_smart"
|
|
||||||
|
|
||||||
//go:generate stringer -type=AttributeStatus
|
//go:generate stringer -type=AttributeStatus
|
||||||
|
// AttributeStatus bitwise flag, 1,2,4,8,16,32,etc
|
||||||
type AttributeStatus uint8
|
type AttributeStatus uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// AttributeStatusPassed binary, 1,2,4,8,16,32,etc
|
|
||||||
AttributeStatusPassed AttributeStatus = 0
|
AttributeStatusPassed AttributeStatus = 0
|
||||||
AttributeStatusFailedSmart AttributeStatus = 1
|
AttributeStatusFailedSmart AttributeStatus = 1
|
||||||
AttributeStatusWarningScrutiny AttributeStatus = 2
|
AttributeStatusWarningScrutiny AttributeStatus = 2
|
||||||
@@ -30,9 +24,10 @@ func AttributeStatusToggle(b, flag AttributeStatus) AttributeStatus { return b ^
|
|||||||
func AttributeStatusHas(b, flag AttributeStatus) bool { return b&flag != 0 }
|
func AttributeStatusHas(b, flag AttributeStatus) bool { return b&flag != 0 }
|
||||||
|
|
||||||
//go:generate stringer -type=DeviceStatus
|
//go:generate stringer -type=DeviceStatus
|
||||||
|
// DeviceStatus bitwise flag, 1,2,4,8,16,32,etc
|
||||||
type DeviceStatus uint8
|
type DeviceStatus uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// DeviceStatusPassed binary, 1,2,4,8,16,32,etc
|
|
||||||
DeviceStatusPassed DeviceStatus = 0
|
DeviceStatusPassed DeviceStatus = 0
|
||||||
DeviceStatusFailedSmart DeviceStatus = 1
|
DeviceStatusFailedSmart DeviceStatus = 1
|
||||||
DeviceStatusFailedScrutiny DeviceStatus = 2
|
DeviceStatusFailedScrutiny DeviceStatus = 2
|
||||||
@@ -42,3 +37,29 @@ func DeviceStatusSet(b, flag DeviceStatus) DeviceStatus { return b | flag }
|
|||||||
func DeviceStatusClear(b, flag DeviceStatus) DeviceStatus { return b &^ flag }
|
func DeviceStatusClear(b, flag DeviceStatus) DeviceStatus { return b &^ flag }
|
||||||
func DeviceStatusToggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag }
|
func DeviceStatusToggle(b, flag DeviceStatus) DeviceStatus { return b ^ flag }
|
||||||
func DeviceStatusHas(b, flag DeviceStatus) bool { return b&flag != 0 }
|
func DeviceStatusHas(b, flag DeviceStatus) bool { return b&flag != 0 }
|
||||||
|
|
||||||
|
// Metrics Specific Filtering & Threshold Constants
|
||||||
|
type MetricsNotifyLevel int64
|
||||||
|
|
||||||
|
const (
|
||||||
|
MetricsNotifyLevelWarn MetricsNotifyLevel = 1
|
||||||
|
MetricsNotifyLevelFail MetricsNotifyLevel = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type MetricsStatusFilterAttributes int64
|
||||||
|
|
||||||
|
const (
|
||||||
|
MetricsStatusFilterAttributesAll MetricsStatusFilterAttributes = 0
|
||||||
|
MetricsStatusFilterAttributesCritical MetricsStatusFilterAttributes = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// MetricsStatusThreshold bitwise flag, 1,2,4,8,16,32,etc
|
||||||
|
type MetricsStatusThreshold int64
|
||||||
|
|
||||||
|
const (
|
||||||
|
MetricsStatusThresholdSmart MetricsStatusThreshold = 1
|
||||||
|
MetricsStatusThresholdScrutiny MetricsStatusThreshold = 2
|
||||||
|
|
||||||
|
//shortcut
|
||||||
|
MetricsStatusThresholdBoth MetricsStatusThreshold = 3
|
||||||
|
)
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ import (
|
|||||||
type DeviceRepo interface {
|
type DeviceRepo interface {
|
||||||
Close() error
|
Close() error
|
||||||
|
|
||||||
//GetSettings()
|
|
||||||
//SaveSetting()
|
|
||||||
|
|
||||||
RegisterDevice(ctx context.Context, dev models.Device) error
|
RegisterDevice(ctx context.Context, dev models.Device) error
|
||||||
GetDevices(ctx context.Context) ([]models.Device, error)
|
GetDevices(ctx context.Context) ([]models.Device, error)
|
||||||
UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error)
|
UpdateDevice(ctx context.Context, wwn string, collectorSmartData collector.SmartInfo) (models.Device, error)
|
||||||
@@ -28,4 +25,7 @@ type DeviceRepo interface {
|
|||||||
|
|
||||||
GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error)
|
GetSummary(ctx context.Context) (map[string]*models.DeviceSummary, error)
|
||||||
GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error)
|
GetSmartTemperatureHistory(ctx context.Context, durationKey string) (map[string][]measurements.SmartTemperature, error)
|
||||||
|
|
||||||
|
LoadSettings(ctx context.Context) (*models.Settings, error)
|
||||||
|
SaveSettings(ctx context.Context, settings models.Settings) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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"`
|
||||||
|
}
|
||||||
@@ -4,9 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20201107210306"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20201107210306"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220503120000"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220509170100"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220509170100"
|
||||||
|
"github.com/analogj/scrutiny/webapp/backend/pkg/database/migrations/m20220716214900"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/measurements"
|
||||||
@@ -275,6 +277,71 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
|||||||
return tx.Where("wwn = ?", "").Delete(&models.Device{}).Error
|
return tx.Where("wwn = ?", "").Delete(&models.Device{}).Error
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: "m20220716214900", // add settings table.
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
|
||||||
|
// adding the settings table.
|
||||||
|
err := tx.AutoMigrate(m20220716214900.Setting{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
//add defaults.
|
||||||
|
|
||||||
|
var defaultSettings = []m20220716214900.Setting{
|
||||||
|
{
|
||||||
|
SettingKeyName: "theme",
|
||||||
|
SettingKeyDescription: "Frontend theme ('light' | 'dark' | 'system')",
|
||||||
|
SettingDataType: "string",
|
||||||
|
SettingValueString: "system", // options: 'light' | 'dark' | 'system'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SettingKeyName: "layout",
|
||||||
|
SettingKeyDescription: "Frontend layout ('material')",
|
||||||
|
SettingDataType: "string",
|
||||||
|
SettingValueString: "material",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SettingKeyName: "dashboard_display",
|
||||||
|
SettingKeyDescription: "Frontend device display title ('name' | 'serial_id' | 'uuid' | 'label')",
|
||||||
|
SettingDataType: "string",
|
||||||
|
SettingValueString: "name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SettingKeyName: "dashboard_sort",
|
||||||
|
SettingKeyDescription: "Frontend device sort by ('status' | 'title' | 'age')",
|
||||||
|
SettingDataType: "string",
|
||||||
|
SettingValueString: "status",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SettingKeyName: "temperature_unit",
|
||||||
|
SettingKeyDescription: "Frontend temperature unit ('celsius' | 'fahrenheit')",
|
||||||
|
SettingDataType: "string",
|
||||||
|
SettingValueString: "celsius",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
SettingKeyName: "metrics.notify_level",
|
||||||
|
SettingKeyDescription: "Determines which device status will cause a notification (fail or warn)",
|
||||||
|
SettingDataType: "numeric",
|
||||||
|
SettingValueNumeric: int(pkg.MetricsNotifyLevelFail), // options: 'fail' or 'warn'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SettingKeyName: "metrics.status_filter_attributes",
|
||||||
|
SettingKeyDescription: "Determines which attributes should impact device status",
|
||||||
|
SettingDataType: "numeric",
|
||||||
|
SettingValueNumeric: int(pkg.MetricsStatusFilterAttributesAll), // options: 'all' or 'critical'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SettingKeyName: "metrics.status_threshold",
|
||||||
|
SettingKeyDescription: "Determines which threshold should impact device status",
|
||||||
|
SettingDataType: "numeric",
|
||||||
|
SettingValueNumeric: int(pkg.MetricsStatusThresholdBoth), // options: 'scrutiny', 'smart', 'both'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return tx.Create(&defaultSettings).Error
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := m.Migrate(); err != nil {
|
if err := m.Migrate(); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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").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,22 @@
|
|||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SettingEntry) TableName() string {
|
||||||
|
return "settings"
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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"`
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
@@ -29,20 +29,22 @@ const NotifyFailureTypeSmartFailure = "SmartFailure"
|
|||||||
const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure"
|
const NotifyFailureTypeScrutinyFailure = "ScrutinyFailure"
|
||||||
|
|
||||||
// ShouldNotify check if the error Message should be filtered (level mismatch or filtered_attributes)
|
// ShouldNotify check if the error Message should be filtered (level mismatch or filtered_attributes)
|
||||||
func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLevel string, notifyFilterAttributes string) bool {
|
func ShouldNotify(device models.Device, smartAttrs measurements.Smart, statusThreshold pkg.MetricsStatusThreshold, statusFilterAttributes pkg.MetricsStatusFilterAttributes) bool {
|
||||||
// 1. check if the device is healthy
|
// 1. check if the device is healthy
|
||||||
if device.DeviceStatus == pkg.DeviceStatusPassed {
|
if device.DeviceStatus == pkg.DeviceStatusPassed {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: cannot check for warning notifyLevel yet.
|
||||||
|
|
||||||
// setup constants for comparison
|
// setup constants for comparison
|
||||||
var requiredDeviceStatus pkg.DeviceStatus
|
var requiredDeviceStatus pkg.DeviceStatus
|
||||||
var requiredAttrStatus pkg.AttributeStatus
|
var requiredAttrStatus pkg.AttributeStatus
|
||||||
if notifyLevel == pkg.NotifyLevelFail {
|
if statusThreshold == pkg.MetricsStatusThresholdBoth {
|
||||||
// either scrutiny or smart failures should trigger an email
|
// either scrutiny or smart failures should trigger an email
|
||||||
requiredDeviceStatus = pkg.DeviceStatusSet(pkg.DeviceStatusFailedSmart, pkg.DeviceStatusFailedScrutiny)
|
requiredDeviceStatus = pkg.DeviceStatusSet(pkg.DeviceStatusFailedSmart, pkg.DeviceStatusFailedScrutiny)
|
||||||
requiredAttrStatus = pkg.AttributeStatusSet(pkg.AttributeStatusFailedSmart, pkg.AttributeStatusFailedScrutiny)
|
requiredAttrStatus = pkg.AttributeStatusSet(pkg.AttributeStatusFailedSmart, pkg.AttributeStatusFailedScrutiny)
|
||||||
} else if notifyLevel == pkg.NotifyLevelFailSmart {
|
} else if statusThreshold == pkg.MetricsStatusThresholdSmart {
|
||||||
//only smart failures
|
//only smart failures
|
||||||
requiredDeviceStatus = pkg.DeviceStatusFailedSmart
|
requiredDeviceStatus = pkg.DeviceStatusFailedSmart
|
||||||
requiredAttrStatus = pkg.AttributeStatusFailedSmart
|
requiredAttrStatus = pkg.AttributeStatusFailedSmart
|
||||||
@@ -53,9 +55,9 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLev
|
|||||||
|
|
||||||
// 2. check if the attributes that are failing should be filtered (non-critical)
|
// 2. check if the attributes that are failing should be filtered (non-critical)
|
||||||
// 3. for any unfiltered attribute, store the failure reason (Smart or Scrutiny)
|
// 3. for any unfiltered attribute, store the failure reason (Smart or Scrutiny)
|
||||||
if notifyFilterAttributes == pkg.NotifyFilterAttributesCritical {
|
if statusFilterAttributes == pkg.MetricsStatusFilterAttributesCritical {
|
||||||
hasFailingCriticalAttr := false
|
hasFailingCriticalAttr := false
|
||||||
var statusFailingCrtiticalAttr pkg.AttributeStatus
|
var statusFailingCriticalAttr pkg.AttributeStatus
|
||||||
|
|
||||||
for attrId, attrData := range smartAttrs.Attributes {
|
for attrId, attrData := range smartAttrs.Attributes {
|
||||||
//find failing attribute
|
//find failing attribute
|
||||||
@@ -64,7 +66,7 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLev
|
|||||||
}
|
}
|
||||||
|
|
||||||
// merge the status's of all critical attributes
|
// merge the status's of all critical attributes
|
||||||
statusFailingCrtiticalAttr = pkg.AttributeStatusSet(statusFailingCrtiticalAttr, attrData.GetStatus())
|
statusFailingCriticalAttr = pkg.AttributeStatusSet(statusFailingCriticalAttr, attrData.GetStatus())
|
||||||
|
|
||||||
//found a failing attribute, see if its critical
|
//found a failing attribute, see if its critical
|
||||||
if device.IsScsi() && thresholds.ScsiMetadata[attrId].Critical {
|
if device.IsScsi() && thresholds.ScsiMetadata[attrId].Critical {
|
||||||
@@ -89,7 +91,7 @@ func ShouldNotify(device models.Device, smartAttrs measurements.Smart, notifyLev
|
|||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
// check if any of the critical attributes have a status that we're looking for
|
// check if any of the critical attributes have a status that we're looking for
|
||||||
return pkg.AttributeStatusHas(statusFailingCrtiticalAttr, requiredAttrStatus)
|
return pkg.AttributeStatusHas(statusFailingCriticalAttr, requiredAttrStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -15,56 +15,56 @@ func TestShouldNotify_MustSkipPassingDevices(t *testing.T) {
|
|||||||
DeviceStatus: pkg.DeviceStatusPassed,
|
DeviceStatus: pkg.DeviceStatusPassed,
|
||||||
}
|
}
|
||||||
smartAttrs := measurements.Smart{}
|
smartAttrs := measurements.Smart{}
|
||||||
notifyLevel := pkg.NotifyLevelFail
|
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||||
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
|
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotify_NotifyLevelFail_FailingSmartDevice(t *testing.T) {
|
func TestShouldNotify_MetricsStatusThresholdBoth_FailingSmartDevice(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//setup
|
//setup
|
||||||
device := models.Device{
|
device := models.Device{
|
||||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||||
}
|
}
|
||||||
smartAttrs := measurements.Smart{}
|
smartAttrs := measurements.Smart{}
|
||||||
notifyLevel := pkg.NotifyLevelFail
|
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||||
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
|
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotify_NotifyLevelFailSmart_FailingSmartDevice(t *testing.T) {
|
func TestShouldNotify_MetricsStatusThresholdSmart_FailingSmartDevice(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//setup
|
//setup
|
||||||
device := models.Device{
|
device := models.Device{
|
||||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||||
}
|
}
|
||||||
smartAttrs := measurements.Smart{}
|
smartAttrs := measurements.Smart{}
|
||||||
notifyLevel := pkg.NotifyLevelFailSmart
|
statusThreshold := pkg.MetricsStatusThresholdSmart
|
||||||
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
|
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotify_NotifyLevelFailScrutiny_FailingSmartDevice(t *testing.T) {
|
func TestShouldNotify_MetricsStatusThresholdScrutiny_FailingSmartDevice(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//setup
|
//setup
|
||||||
device := models.Device{
|
device := models.Device{
|
||||||
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
DeviceStatus: pkg.DeviceStatusFailedSmart,
|
||||||
}
|
}
|
||||||
smartAttrs := measurements.Smart{}
|
smartAttrs := measurements.Smart{}
|
||||||
notifyLevel := pkg.NotifyLevelFailScrutiny
|
statusThreshold := pkg.MetricsStatusThresholdScrutiny
|
||||||
notifyFilterAttributes := pkg.NotifyFilterAttributesAll
|
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesAll
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotify_NotifyFilterAttributesCritical_WithCriticalAttrs(t *testing.T) {
|
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithCriticalAttrs(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//setup
|
//setup
|
||||||
device := models.Device{
|
device := models.Device{
|
||||||
@@ -75,14 +75,14 @@ func TestShouldNotify_NotifyFilterAttributesCritical_WithCriticalAttrs(t *testin
|
|||||||
Status: pkg.AttributeStatusFailedSmart,
|
Status: pkg.AttributeStatusFailedSmart,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
notifyLevel := pkg.NotifyLevelFail
|
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||||
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotify_NotifyFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) {
|
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithMultipleCriticalAttrs(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//setup
|
//setup
|
||||||
device := models.Device{
|
device := models.Device{
|
||||||
@@ -96,14 +96,14 @@ func TestShouldNotify_NotifyFilterAttributesCritical_WithMultipleCriticalAttrs(t
|
|||||||
Status: pkg.AttributeStatusFailedScrutiny,
|
Status: pkg.AttributeStatusFailedScrutiny,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
notifyLevel := pkg.NotifyLevelFail
|
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||||
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
require.True(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
require.True(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotify_NotifyFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) {
|
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoCriticalAttrs(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//setup
|
//setup
|
||||||
device := models.Device{
|
device := models.Device{
|
||||||
@@ -114,14 +114,14 @@ func TestShouldNotify_NotifyFilterAttributesCritical_WithNoCriticalAttrs(t *test
|
|||||||
Status: pkg.AttributeStatusFailedSmart,
|
Status: pkg.AttributeStatusFailedSmart,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
notifyLevel := pkg.NotifyLevelFail
|
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||||
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotify_NotifyFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) {
|
func TestShouldNotify_MetricsStatusFilterAttributesCritical_WithNoFailingCriticalAttrs(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//setup
|
//setup
|
||||||
device := models.Device{
|
device := models.Device{
|
||||||
@@ -132,14 +132,14 @@ func TestShouldNotify_NotifyFilterAttributesCritical_WithNoFailingCriticalAttrs(
|
|||||||
Status: pkg.AttributeStatusPassed,
|
Status: pkg.AttributeStatusPassed,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
notifyLevel := pkg.NotifyLevelFail
|
statusThreshold := pkg.MetricsStatusThresholdBoth
|
||||||
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldNotify_NotifyFilterAttributesCritical_NotifyLevelFailSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) {
|
func TestShouldNotify_MetricsStatusFilterAttributesCritical_MetricsStatusThresholdSmart_WithCriticalAttrsFailingScrutiny(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//setup
|
//setup
|
||||||
device := models.Device{
|
device := models.Device{
|
||||||
@@ -153,9 +153,9 @@ func TestShouldNotify_NotifyFilterAttributesCritical_NotifyLevelFailSmart_WithCr
|
|||||||
Status: pkg.AttributeStatusFailedScrutiny,
|
Status: pkg.AttributeStatusFailedScrutiny,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
notifyLevel := pkg.NotifyLevelFailSmart
|
statusThreshold := pkg.MetricsStatusThresholdSmart
|
||||||
notifyFilterAttributes := pkg.NotifyFilterAttributesCritical
|
notifyFilterAttributes := pkg.MetricsStatusFilterAttributesCritical
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
require.False(t, ShouldNotify(device, smartAttrs, notifyLevel, notifyFilterAttributes))
|
require.False(t, ShouldNotify(device, smartAttrs, statusThreshold, notifyFilterAttributes))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.FieldLogger)
|
||||||
|
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.FieldLogger)
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||||
@@ -67,7 +68,12 @@ func UploadDeviceMetrics(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//check for error
|
//check for error
|
||||||
if notify.ShouldNotify(updatedDevice, smartData, appConfig.GetString("notify.level"), appConfig.GetString("notify.filter_attributes")) {
|
if notify.ShouldNotify(
|
||||||
|
updatedDevice,
|
||||||
|
smartData,
|
||||||
|
pkg.MetricsStatusThreshold(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY))),
|
||||||
|
pkg.MetricsStatusFilterAttributes(appConfig.GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY))),
|
||||||
|
) {
|
||||||
//send notifications
|
//send notifications
|
||||||
|
|
||||||
liveNotify := notify.New(
|
liveNotify := notify.New(
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func LoggerMiddleware(logger logrus.FieldLogger) gin.HandlerFunc {
|
|||||||
|
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hostname = "unknow"
|
hostname = "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -14,6 +15,14 @@ func RepositoryMiddleware(appConfig config.Interface, globalLogger logrus.FieldL
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensure the settings have been loaded into the app config during startup.
|
||||||
|
_, err = deviceRepo.LoadSettings(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//settings.UpdateSettingEntries()
|
||||||
|
|
||||||
//TODO: determine where we can call defer deviceRepo.Close()
|
//TODO: determine where we can call defer deviceRepo.Close()
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
c.Set("DEVICE_REPOSITORY", deviceRepo)
|
c.Set("DEVICE_REPOSITORY", deviceRepo)
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine {
|
|||||||
api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details
|
api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details
|
||||||
api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device
|
api.DELETE("/device/:wwn", handler.DeleteDevice) //used by UI to delete device
|
||||||
|
|
||||||
|
api.GET("/settings", handler.GetSettings) //used to get settings
|
||||||
|
api.POST("/settings", handler.SaveSettings) //used to save settings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package web_test
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||||
|
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||||
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
|
mock_config "github.com/analogj/scrutiny/webapp/backend/pkg/config/mock"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models"
|
||||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||||
@@ -89,6 +91,8 @@ func (suite *ServerTestSuite) TestHealthRoute() {
|
|||||||
mockCtrl := gomock.NewController(suite.T())
|
mockCtrl := gomock.NewController(suite.T())
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
|
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||||
fakeConfig.EXPECT().GetString("web.database.location").Return(path.Join(parentPath, "scrutiny_test.db")).AnyTimes()
|
fakeConfig.EXPECT().GetString("web.database.location").Return(path.Join(parentPath, "scrutiny_test.db")).AnyTimes()
|
||||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
|
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
|
||||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||||
@@ -130,6 +134,8 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
|
|||||||
mockCtrl := gomock.NewController(suite.T())
|
mockCtrl := gomock.NewController(suite.T())
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
|
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||||
fakeConfig.EXPECT().GetString("web.database.location").Return(path.Join(parentPath, "scrutiny_test.db")).AnyTimes()
|
fakeConfig.EXPECT().GetString("web.database.location").Return(path.Join(parentPath, "scrutiny_test.db")).AnyTimes()
|
||||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
|
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
|
||||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||||
@@ -170,6 +176,8 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
|
|||||||
mockCtrl := gomock.NewController(suite.T())
|
mockCtrl := gomock.NewController(suite.T())
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
|
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||||
@@ -186,8 +194,9 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
|
|||||||
} else {
|
} else {
|
||||||
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("localhost").AnyTimes()
|
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("localhost").AnyTimes()
|
||||||
}
|
}
|
||||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
|
||||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
|
||||||
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
|
||||||
|
|
||||||
ae := web.AppEngine{
|
ae := web.AppEngine{
|
||||||
Config: fakeConfig,
|
Config: fakeConfig,
|
||||||
@@ -219,10 +228,13 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
|
|||||||
mockCtrl := gomock.NewController(suite.T())
|
mockCtrl := gomock.NewController(suite.T())
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
|
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||||
//fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return("testdata/scrutiny_test.db")
|
//fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return("testdata/scrutiny_test.db")
|
||||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").Return([]string{}).AnyTimes()
|
fakeConfig.EXPECT().GetStringSlice("notify.urls").Return([]string{}).AnyTimes()
|
||||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
|
||||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
|
||||||
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
|
||||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||||
@@ -319,6 +331,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
|
|||||||
mockCtrl := gomock.NewController(suite.T())
|
mockCtrl := gomock.NewController(suite.T())
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
|
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||||
@@ -330,8 +344,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
|
|||||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"https://unroutable.domain.example.asdfghj"})
|
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"https://unroutable.domain.example.asdfghj"})
|
||||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
|
||||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
|
||||||
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
|
||||||
|
|
||||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||||
@@ -361,6 +376,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
|
|||||||
mockCtrl := gomock.NewController(suite.T())
|
mockCtrl := gomock.NewController(suite.T())
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
|
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||||
@@ -372,8 +389,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
|
|||||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///missing/path/on/disk"})
|
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///missing/path/on/disk"})
|
||||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
|
||||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
|
||||||
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
|
||||||
|
|
||||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||||
@@ -403,6 +421,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
|
|||||||
mockCtrl := gomock.NewController(suite.T())
|
mockCtrl := gomock.NewController(suite.T())
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
|
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||||
@@ -414,8 +434,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
|
|||||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///usr/bin/env"})
|
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"script:///usr/bin/env"})
|
||||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
|
||||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
|
||||||
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
|
||||||
|
|
||||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||||
@@ -445,6 +466,8 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
|
|||||||
mockCtrl := gomock.NewController(suite.T())
|
mockCtrl := gomock.NewController(suite.T())
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
|
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||||
@@ -456,8 +479,9 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
|
|||||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"discord://invalidtoken@channel"})
|
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{"discord://invalidtoken@channel"})
|
||||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
|
||||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
|
||||||
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
|
||||||
|
|
||||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||||
@@ -486,6 +510,8 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
|||||||
mockCtrl := gomock.NewController(suite.T())
|
mockCtrl := gomock.NewController(suite.T())
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||||
|
fakeConfig.EXPECT().SetDefault(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
fakeConfig.EXPECT().UnmarshalKey(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
|
||||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(path.Join(parentPath, "scrutiny_test.db"))
|
||||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
fakeConfig.EXPECT().GetString("web.src.frontend.path").AnyTimes().Return(parentPath)
|
||||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||||
@@ -497,8 +523,10 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
|||||||
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
fakeConfig.EXPECT().GetString("web.influxdb.bucket").Return("metrics").AnyTimes()
|
||||||
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
fakeConfig.EXPECT().GetBool("web.influxdb.retention_policy").Return(false).AnyTimes()
|
||||||
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{})
|
fakeConfig.EXPECT().GetStringSlice("notify.urls").AnyTimes().Return([]string{})
|
||||||
fakeConfig.EXPECT().GetString("notify.level").AnyTimes().Return(pkg.NotifyLevelFail)
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.notify_level", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsNotifyLevelFail))
|
||||||
fakeConfig.EXPECT().GetString("notify.filter_attributes").AnyTimes().Return(pkg.NotifyFilterAttributesAll)
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_filter_attributes", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusFilterAttributesAll))
|
||||||
|
fakeConfig.EXPECT().GetInt(fmt.Sprintf("%s.metrics.status_threshold", config.DB_USER_SETTINGS_SUBKEY)).AnyTimes().Return(int(pkg.MetricsStatusThresholdBoth))
|
||||||
|
|
||||||
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
if _, isGithubActions := os.LookupEnv("GITHUB_ACTIONS"); isGithubActions {
|
||||||
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
// when running test suite in github actions, we run an influxdb service as a sidecar.
|
||||||
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("influxdb").AnyTimes()
|
fakeConfig.EXPECT().GetString("web.influxdb.host").Return("influxdb").AnyTimes()
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { NgModule, enableProdMode } from '@angular/core';
|
import {enableProdMode, NgModule} from '@angular/core';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
import { ExtraOptions, PreloadAllModules, RouterModule } from '@angular/router';
|
import {ExtraOptions, PreloadAllModules, RouterModule} from '@angular/router';
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
import {APP_BASE_HREF} from '@angular/common';
|
||||||
import { MarkdownModule } from 'ngx-markdown';
|
import {MarkdownModule} from 'ngx-markdown';
|
||||||
import { TreoModule } from '@treo';
|
import {TreoModule} from '@treo';
|
||||||
import { TreoConfigModule } from '@treo/services/config';
|
import {ScrutinyConfigModule} from 'app/core/config/scrutiny-config.module';
|
||||||
import { TreoMockApiModule } from '@treo/lib/mock-api';
|
import {TreoMockApiModule} from '@treo/lib/mock-api';
|
||||||
import { CoreModule } from 'app/core/core.module';
|
import {CoreModule} from 'app/core/core.module';
|
||||||
import { appConfig } from 'app/core/config/app.config';
|
import {appConfig} from 'app/core/config/app.config';
|
||||||
import { mockDataServices } from 'app/data/mock';
|
import {mockDataServices} from 'app/data/mock';
|
||||||
import { LayoutModule } from 'app/layout/layout.module';
|
import {LayoutModule} from 'app/layout/layout.module';
|
||||||
import { AppComponent } from 'app/app.component';
|
import {AppComponent} from 'app/app.component';
|
||||||
import { appRoutes, getAppBaseHref } from 'app/app.routing';
|
import {appRoutes, getAppBaseHref} from 'app/app.routing';
|
||||||
|
|
||||||
const routerConfig: ExtraOptions = {
|
const routerConfig: ExtraOptions = {
|
||||||
scrollPositionRestoration: 'enabled',
|
scrollPositionRestoration: 'enabled',
|
||||||
preloadingStrategy : PreloadAllModules
|
preloadingStrategy: PreloadAllModules
|
||||||
};
|
};
|
||||||
|
|
||||||
let dev = [
|
let dev = [
|
||||||
@@ -41,7 +41,7 @@ if (process.env.NODE_ENV === 'production') {
|
|||||||
|
|
||||||
// Treo & Treo Mock API
|
// Treo & Treo Mock API
|
||||||
TreoModule,
|
TreoModule,
|
||||||
TreoConfigModule.forRoot(appConfig),
|
ScrutinyConfigModule.forRoot(appConfig),
|
||||||
...dev,
|
...dev,
|
||||||
|
|
||||||
// Core
|
// Core
|
||||||
|
|||||||
@@ -10,19 +10,47 @@ export type DashboardSort = 'status' | 'title' | 'age'
|
|||||||
|
|
||||||
export type TemperatureUnit = 'celsius' | 'fahrenheit'
|
export type TemperatureUnit = 'celsius' | 'fahrenheit'
|
||||||
|
|
||||||
|
|
||||||
|
export enum MetricsNotifyLevel {
|
||||||
|
Warn = 1,
|
||||||
|
Fail = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MetricsStatusFilterAttributes {
|
||||||
|
All = 0,
|
||||||
|
Critical = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MetricsStatusThreshold {
|
||||||
|
Smart = 1,
|
||||||
|
Scrutiny = 2,
|
||||||
|
|
||||||
|
// shortcut
|
||||||
|
Both = 3
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AppConfig interface. Update this interface to strictly type your config
|
* AppConfig interface. Update this interface to strictly type your config
|
||||||
* object.
|
* object.
|
||||||
*/
|
*/
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
theme: Theme;
|
theme?: Theme;
|
||||||
layout: Layout;
|
layout?: Layout;
|
||||||
|
|
||||||
// Dashboard options
|
// Dashboard options
|
||||||
dashboardDisplay: DashboardDisplay;
|
dashboard_display?: DashboardDisplay;
|
||||||
dashboardSort: DashboardSort;
|
dashboard_sort?: DashboardSort;
|
||||||
|
|
||||||
|
temperature_unit?: TemperatureUnit;
|
||||||
|
|
||||||
|
// Settings from Scrutiny API
|
||||||
|
|
||||||
|
metrics?: {
|
||||||
|
notify_level?: MetricsNotifyLevel
|
||||||
|
status_filter_attributes?: MetricsStatusFilterAttributes
|
||||||
|
status_threshold?: MetricsStatusThreshold
|
||||||
|
}
|
||||||
|
|
||||||
temperatureUnit: TemperatureUnit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,12 +62,17 @@ export interface AppConfig {
|
|||||||
* "ConfigService".
|
* "ConfigService".
|
||||||
*/
|
*/
|
||||||
export const appConfig: AppConfig = {
|
export const appConfig: AppConfig = {
|
||||||
theme : 'light',
|
theme: 'light',
|
||||||
layout: 'material',
|
layout: 'material',
|
||||||
|
|
||||||
dashboardDisplay: 'name',
|
dashboard_display: 'name',
|
||||||
dashboardSort: 'status',
|
dashboard_sort: 'status',
|
||||||
|
|
||||||
temperatureUnit: 'celsius',
|
temperature_unit: 'celsius',
|
||||||
|
metrics: {
|
||||||
|
notify_level: MetricsNotifyLevel.Fail,
|
||||||
|
status_filter_attributes: MetricsStatusFilterAttributes.All,
|
||||||
|
status_threshold: MetricsStatusThreshold.Both
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-8
@@ -1,21 +1,21 @@
|
|||||||
<div [ngClass]="{ 'border-green': deviceSummary.device.device_status == 0 && deviceSummary.smart,
|
<div [ngClass]="{ 'border-green': deviceStatusString(deviceSummary) == 'passed',
|
||||||
'border-red': deviceSummary.device.device_status != 0 }"
|
'border-red': deviceStatusString(deviceSummary) == 'failed' }"
|
||||||
class="relative flex flex-col flex-auto p-6 pr-3 pb-3 bg-card rounded border-l-4 shadow-md overflow-hidden">
|
class="relative flex flex-col flex-auto p-6 pr-3 pb-3 bg-card rounded border-l-4 shadow-md overflow-hidden">
|
||||||
<div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
|
<div class="absolute bottom-0 right-0 w-24 h-24 -m-6">
|
||||||
<mat-icon class="icon-size-96 opacity-12 text-green"
|
<mat-icon class="icon-size-96 opacity-12 text-green"
|
||||||
*ngIf="deviceSummary.device.device_status == 0 && deviceSummary.smart"
|
*ngIf="deviceStatusString(deviceSummary) == 'passed'"
|
||||||
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
|
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
|
||||||
<mat-icon class="icon-size-96 opacity-12 text-red"
|
<mat-icon class="icon-size-96 opacity-12 text-red"
|
||||||
*ngIf="deviceSummary.device.device_status != 0"
|
*ngIf="deviceStatusString(deviceSummary) == 'failed'"
|
||||||
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
|
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
|
||||||
<mat-icon class="icon-size-96 opacity-12 text-yellow"
|
<mat-icon class="icon-size-96 opacity-12 text-yellow"
|
||||||
*ngIf="!deviceSummary.smart"
|
*ngIf="deviceStatusString(deviceSummary) == 'unknown'"
|
||||||
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
|
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<a [routerLink]="'/device/'+ deviceSummary.device.wwn"
|
<a [routerLink]="'/device/'+ deviceSummary.device.wwn"
|
||||||
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceSummary.device | deviceTitle:config.dashboardDisplay}}</a>
|
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceSummary.device | deviceTitle:config.dashboard_display}}</a>
|
||||||
<div [ngClass]="classDeviceLastUpdatedOn(deviceSummary)" class="font-medium text-sm" *ngIf="deviceSummary.smart">
|
<div [ngClass]="classDeviceLastUpdatedOn(deviceSummary)" class="font-medium text-sm" *ngIf="deviceSummary.smart">
|
||||||
Last Updated on {{deviceSummary.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
|
Last Updated on {{deviceSummary.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
|
||||||
</div>
|
</div>
|
||||||
@@ -46,12 +46,14 @@
|
|||||||
<div class="flex flex-row flex-wrap mt-4 -mx-6">
|
<div class="flex flex-row flex-wrap mt-4 -mx-6">
|
||||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Status</div>
|
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Status</div>
|
||||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.collector_date; else unknownStatus">{{ deviceStatusString(deviceSummary.device.device_status) | titlecase}}</div>
|
<div class="mt-2 font-medium text-3xl leading-none"
|
||||||
|
*ngIf="deviceSummary.smart?.collector_date; else unknownStatus">{{ deviceStatusString(deviceSummary) | titlecase}}</div>
|
||||||
<ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
|
<ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||||
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Temperature</div>
|
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Temperature</div>
|
||||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.collector_date; else unknownTemp">{{ deviceSummary.smart?.temp | temperature:config.temperatureUnit:true }}</div>
|
<div class="mt-2 font-medium text-3xl leading-none"
|
||||||
|
*ngIf="deviceSummary.smart?.collector_date; else unknownTemp">{{ deviceSummary.smart?.temp | temperature:config.temperature_unit:true }}</div>
|
||||||
<ng-template #unknownTemp><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
|
<ng-template #unknownTemp><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
<div class="flex flex-col mx-6 my-3 xs:w-full">
|
||||||
|
|||||||
+4
-2
@@ -9,13 +9,14 @@ import {MatMenuModule} from '@angular/material/menu';
|
|||||||
import {TREO_APP_CONFIG} from '@treo/services/config/config.constants';
|
import {TREO_APP_CONFIG} from '@treo/services/config/config.constants';
|
||||||
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
|
import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
|
||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
|
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||||
|
|
||||||
describe('DashboardDeviceComponent', () => {
|
describe('DashboardDeviceComponent', () => {
|
||||||
let component: DashboardDeviceComponent;
|
let component: DashboardDeviceComponent;
|
||||||
let fixture: ComponentFixture<DashboardDeviceComponent>;
|
let fixture: ComponentFixture<DashboardDeviceComponent>;
|
||||||
|
|
||||||
const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']);
|
const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']);
|
||||||
// const configServiceSpy = jasmine.createSpyObj('TreoConfigService', ['config$']);
|
// const configServiceSpy = jasmine.createSpyObj('ScrutinyConfigService', ['config$']);
|
||||||
|
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
@@ -25,10 +26,11 @@ describe('DashboardDeviceComponent', () => {
|
|||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
HttpClientTestingModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{provide: MatDialog, useValue: matDialogSpy},
|
{provide: MatDialog, useValue: matDialogSpy},
|
||||||
{provide: TREO_APP_CONFIG, useValue: {dashboardDisplay: 'name'}}
|
{provide: TREO_APP_CONFIG, useValue: {dashboard_display: 'name'}}
|
||||||
],
|
],
|
||||||
declarations: [DashboardDeviceComponent]
|
declarations: [DashboardDeviceComponent]
|
||||||
})
|
})
|
||||||
|
|||||||
+12
-4
@@ -2,7 +2,7 @@ import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
|||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
import {takeUntil} from 'rxjs/operators';
|
import {takeUntil} from 'rxjs/operators';
|
||||||
import {AppConfig} from 'app/core/config/app.config';
|
import {AppConfig} from 'app/core/config/app.config';
|
||||||
import {TreoConfigService} from '@treo/services/config';
|
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
|
||||||
import {Subject} from 'rxjs';
|
import {Subject} from 'rxjs';
|
||||||
import humanizeDuration from 'humanize-duration'
|
import humanizeDuration from 'humanize-duration'
|
||||||
import {MatDialog} from '@angular/material/dialog';
|
import {MatDialog} from '@angular/material/dialog';
|
||||||
@@ -18,7 +18,7 @@ import {DeviceSummaryModel} from 'app/core/models/device-summary-model';
|
|||||||
export class DashboardDeviceComponent implements OnInit {
|
export class DashboardDeviceComponent implements OnInit {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _configService: TreoConfigService,
|
private _configService: ScrutinyConfigService,
|
||||||
public dialog: MatDialog,
|
public dialog: MatDialog,
|
||||||
) {
|
) {
|
||||||
// Set the private defaults
|
// Set the private defaults
|
||||||
@@ -68,7 +68,15 @@ export class DashboardDeviceComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceStatusString(deviceStatus: number): string {
|
deviceStatusString(deviceSummary: DeviceSummaryModel): string {
|
||||||
|
// no smart data, so treat the device status as unknown
|
||||||
|
if (!deviceSummary.smart) {
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine the device status, by comparing it against the allowed threshold
|
||||||
|
// tslint:disable-next-line:no-bitwise
|
||||||
|
const deviceStatus = deviceSummary.device.device_status & this.config.metrics.status_threshold
|
||||||
if (deviceStatus === 0) {
|
if (deviceStatus === 0) {
|
||||||
return 'passed'
|
return 'passed'
|
||||||
} else {
|
} else {
|
||||||
@@ -82,7 +90,7 @@ export class DashboardDeviceComponent implements OnInit {
|
|||||||
// width: '250px',
|
// width: '250px',
|
||||||
data: {
|
data: {
|
||||||
wwn: this.deviceWWN,
|
wwn: this.deviceWWN,
|
||||||
title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboardDisplay)
|
title: DeviceTitlePipe.deviceTitleWithFallback(this.deviceSummary.device, this.config.dashboard_display)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+18
-54
@@ -43,63 +43,27 @@
|
|||||||
<mat-option value="fahrenheit">Fahrenheit</mat-option>
|
<mat-option value="fahrenheit">Fahrenheit</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex">
|
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||||
<mat-tab-group mat-align-tabs="start">
|
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
|
||||||
<mat-tab label="Ata">
|
<mat-label>Device Status - Thresholds</mat-label>
|
||||||
|
<mat-select [(ngModel)]=statusThreshold>
|
||||||
|
<mat-option [value]=1>Smart</mat-option>
|
||||||
|
<mat-option [value]=2>Scrutiny</mat-option>
|
||||||
|
<mat-option [value]=3>Both</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div matTooltip="not yet implemented" class="flex flex-col mt-5 gt-md:flex-row">
|
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||||
<mat-form-field class="flex-auto gt-md:pr-3">
|
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
|
||||||
<mat-label class="text-hint">Critical Error Threshold</mat-label>
|
<mat-label>Notify - Filter Attributes</mat-label>
|
||||||
<input disabled matInput [value]="'10%'">
|
<mat-select [(ngModel)]=statusFilterAttributes>
|
||||||
</mat-form-field>
|
<mat-option [value]=0>All</mat-option>
|
||||||
<mat-form-field class="flex-auto gt-md:pl-3">
|
<mat-option [value]=1>Critical</mat-option>
|
||||||
<mat-label class="text-hint">Critical Warning Threshold</mat-label>
|
</mat-select>
|
||||||
<input disabled matInput>
|
</mat-form-field>
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div matTooltip="not yet implemented" class="flex flex-col gt-md:flex-row">
|
|
||||||
<mat-form-field class="flex-auto gt-md:pr-3">
|
|
||||||
<mat-label class="text-hint">Error Threshold</mat-label>
|
|
||||||
<input disabled matInput [value]="'20%'">
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field class="flex-auto gt-md:pl-3">
|
|
||||||
<mat-label class="text-hint">Warning Threshold</mat-label>
|
|
||||||
<input disabled matInput [value]="'10%'">
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</mat-tab>
|
|
||||||
<mat-tab label="NVMe">
|
|
||||||
|
|
||||||
<div matTooltip="not yet implemented" class="flex flex-col mt-5 gt-md:flex-row">
|
|
||||||
<mat-form-field class="flex-auto gt-md:pr-3">
|
|
||||||
<mat-label class="text-hint">Critical Error Threshold</mat-label>
|
|
||||||
<input disabled matInput [value]="'enabled'">
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field class="flex-auto gt-md:pl-3">
|
|
||||||
<mat-label class="text-hint">Critical Warning Threshold</mat-label>
|
|
||||||
<input disabled matInput>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</mat-tab>
|
|
||||||
<mat-tab label="SCSI">
|
|
||||||
<div matTooltip="not yet implemented" class="flex flex-col mt-5 gt-md:flex-row">
|
|
||||||
<mat-form-field class="flex-auto gt-md:pr-3">
|
|
||||||
<mat-label class="text-hint">Critical Error Threshold</mat-label>
|
|
||||||
<input disabled matInput [value]="'enabled'">
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field class="flex-auto gt-md:pl-3">
|
|
||||||
<mat-label class="text-hint">Critical Warning Threshold</mat-label>
|
|
||||||
<input disabled matInput>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</mat-tab>
|
|
||||||
</mat-tab-group>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+28
-11
@@ -1,6 +1,14 @@
|
|||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {AppConfig} from 'app/core/config/app.config';
|
import {
|
||||||
import {TreoConfigService} from '@treo/services/config';
|
AppConfig,
|
||||||
|
DashboardDisplay,
|
||||||
|
DashboardSort,
|
||||||
|
MetricsStatusFilterAttributes,
|
||||||
|
MetricsStatusThreshold,
|
||||||
|
TemperatureUnit,
|
||||||
|
Theme
|
||||||
|
} from 'app/core/config/app.config';
|
||||||
|
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
|
||||||
import {Subject} from 'rxjs';
|
import {Subject} from 'rxjs';
|
||||||
import {takeUntil} from 'rxjs/operators';
|
import {takeUntil} from 'rxjs/operators';
|
||||||
|
|
||||||
@@ -15,12 +23,14 @@ export class DashboardSettingsComponent implements OnInit {
|
|||||||
dashboardSort: string;
|
dashboardSort: string;
|
||||||
temperatureUnit: string;
|
temperatureUnit: string;
|
||||||
theme: string;
|
theme: string;
|
||||||
|
statusThreshold: number;
|
||||||
|
statusFilterAttributes: number;
|
||||||
|
|
||||||
// Private
|
// Private
|
||||||
private _unsubscribeAll: Subject<any>;
|
private _unsubscribeAll: Subject<any>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _configService: TreoConfigService,
|
private _configService: ScrutinyConfigService,
|
||||||
) {
|
) {
|
||||||
// Set the private defaults
|
// Set the private defaults
|
||||||
this._unsubscribeAll = new Subject();
|
this._unsubscribeAll = new Subject();
|
||||||
@@ -33,21 +43,28 @@ export class DashboardSettingsComponent implements OnInit {
|
|||||||
.subscribe((config: AppConfig) => {
|
.subscribe((config: AppConfig) => {
|
||||||
|
|
||||||
// Store the config
|
// Store the config
|
||||||
this.dashboardDisplay = config.dashboardDisplay;
|
this.dashboardDisplay = config.dashboard_display;
|
||||||
this.dashboardSort = config.dashboardSort;
|
this.dashboardSort = config.dashboard_sort;
|
||||||
this.temperatureUnit = config.temperatureUnit;
|
this.temperatureUnit = config.temperature_unit;
|
||||||
this.theme = config.theme;
|
this.theme = config.theme;
|
||||||
|
|
||||||
|
this.statusFilterAttributes = config.metrics.status_filter_attributes;
|
||||||
|
this.statusThreshold = config.metrics.status_threshold;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSettings(): void {
|
saveSettings(): void {
|
||||||
const newSettings = {
|
const newSettings: AppConfig = {
|
||||||
dashboardDisplay: this.dashboardDisplay,
|
dashboard_display: this.dashboardDisplay as DashboardDisplay,
|
||||||
dashboardSort: this.dashboardSort,
|
dashboard_sort: this.dashboardSort as DashboardSort,
|
||||||
temperatureUnit: this.temperatureUnit,
|
temperature_unit: this.temperatureUnit as TemperatureUnit,
|
||||||
theme: this.theme
|
theme: this.theme as Theme,
|
||||||
|
metrics: {
|
||||||
|
status_filter_attributes: this.statusFilterAttributes as MetricsStatusFilterAttributes,
|
||||||
|
status_threshold: this.statusThreshold as MetricsStatusThreshold
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this._configService.config = newSettings
|
this._configService.config = newSettings
|
||||||
console.log(`Saved Settings: ${JSON.stringify(newSettings)}`)
|
console.log(`Saved Settings: ${JSON.stringify(newSettings)}`)
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
import { Component, Inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
import {Component, Inject, OnDestroy, OnInit, ViewEncapsulation} from '@angular/core';
|
||||||
import { DOCUMENT } from '@angular/common';
|
import {DOCUMENT} from '@angular/common';
|
||||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
import {ActivatedRoute, NavigationEnd, Router} from '@angular/router';
|
||||||
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
import {MatSlideToggleChange} from '@angular/material/slide-toggle';
|
||||||
import { Subject } from 'rxjs';
|
import {Subject} from 'rxjs';
|
||||||
import { filter, takeUntil } from 'rxjs/operators';
|
import {filter, takeUntil} from 'rxjs/operators';
|
||||||
import { TreoConfigService } from '@treo/services/config';
|
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
|
||||||
import { TreoDrawerService } from '@treo/components/drawer';
|
import {TreoDrawerService} from '@treo/components/drawer';
|
||||||
import { Layout } from 'app/layout/layout.types';
|
import {Layout} from 'app/layout/layout.types';
|
||||||
import { AppConfig, Theme } from 'app/core/config/app.config';
|
import {AppConfig, Theme} from 'app/core/config/app.config';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector : 'layout',
|
selector: 'layout',
|
||||||
templateUrl : './layout.component.html',
|
templateUrl: './layout.component.html',
|
||||||
styleUrls : ['./layout.component.scss'],
|
styleUrls: ['./layout.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class LayoutComponent implements OnInit, OnDestroy
|
export class LayoutComponent implements OnInit, OnDestroy {
|
||||||
{
|
|
||||||
config: AppConfig;
|
config: AppConfig;
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
@@ -29,14 +28,14 @@ export class LayoutComponent implements OnInit, OnDestroy
|
|||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
* @param {ActivatedRoute} _activatedRoute
|
* @param {ActivatedRoute} _activatedRoute
|
||||||
* @param {TreoConfigService} _treoConfigService
|
* @param {ScrutinyConfigService} _scrutinyConfigService
|
||||||
* @param {TreoDrawerService} _treoDrawerService
|
* @param {TreoDrawerService} _treoDrawerService
|
||||||
* @param {DOCUMENT} _document
|
* @param {DOCUMENT} _document
|
||||||
* @param {Router} _router
|
* @param {Router} _router
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private _activatedRoute: ActivatedRoute,
|
private _activatedRoute: ActivatedRoute,
|
||||||
private _treoConfigService: TreoConfigService,
|
private _scrutinyConfigService: ScrutinyConfigService,
|
||||||
private _treoDrawerService: TreoDrawerService,
|
private _treoDrawerService: TreoDrawerService,
|
||||||
@Inject(DOCUMENT) private _document: any,
|
@Inject(DOCUMENT) private _document: any,
|
||||||
private _router: Router
|
private _router: Router
|
||||||
@@ -59,7 +58,7 @@ export class LayoutComponent implements OnInit, OnDestroy
|
|||||||
ngOnInit(): void
|
ngOnInit(): void
|
||||||
{
|
{
|
||||||
// Subscribe to config changes
|
// Subscribe to config changes
|
||||||
this._treoConfigService.config$
|
this._scrutinyConfigService.config$
|
||||||
.pipe(takeUntil(this._unsubscribeAll))
|
.pipe(takeUntil(this._unsubscribeAll))
|
||||||
.subscribe((config: AppConfig) => {
|
.subscribe((config: AppConfig) => {
|
||||||
|
|
||||||
@@ -180,18 +179,17 @@ export class LayoutComponent implements OnInit, OnDestroy
|
|||||||
*
|
*
|
||||||
* @param layout
|
* @param layout
|
||||||
*/
|
*/
|
||||||
setLayout(layout: string): void
|
setLayout(layout: Layout): void {
|
||||||
{
|
|
||||||
// Clear the 'layout' query param to allow layout changes
|
// Clear the 'layout' query param to allow layout changes
|
||||||
this._router.navigate([], {
|
this._router.navigate([], {
|
||||||
queryParams : {
|
queryParams: {
|
||||||
layout: null
|
layout: null
|
||||||
},
|
},
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
|
||||||
// Set the config
|
// Set the config
|
||||||
this._treoConfigService.config = {layout};
|
this._scrutinyConfigService.config = {layout};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +200,6 @@ export class LayoutComponent implements OnInit, OnDestroy
|
|||||||
*/
|
*/
|
||||||
setTheme(change: MatSlideToggleChange): void
|
setTheme(change: MatSlideToggleChange): void
|
||||||
{
|
{
|
||||||
this._treoConfigService.config = {theme: change.checked ? 'dark' : 'light'};
|
this._scrutinyConfigService.config = {theme: change.checked ? 'dark' : 'light'};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,11 @@
|
|||||||
<div class="flex flex-wrap w-full" *ngFor="let hostId of hostGroups | keyvalue">
|
<div class="flex flex-wrap w-full" *ngFor="let hostId of hostGroups | keyvalue">
|
||||||
<h3 class="ml-4" *ngIf="hostId.key">{{hostId.key}}</h3>
|
<h3 class="ml-4" *ngIf="hostId.key">{{hostId.key}}</h3>
|
||||||
<div class="flex flex-wrap w-full">
|
<div class="flex flex-wrap w-full">
|
||||||
<app-dashboard-device (deviceDeleted)="onDeviceDeleted($event)" class="flex gt-sm:w-1/2 min-w-80 p-4" *ngFor="let deviceSummary of (deviceSummariesForHostGroup(hostId.value) | deviceSort:config.dashboardSort:config.dashboardDisplay )" [deviceWWN]="deviceSummary.device.wwn" [deviceSummary]="deviceSummary"></app-dashboard-device>
|
<app-dashboard-device (deviceDeleted)="onDeviceDeleted($event)"
|
||||||
|
class="flex gt-sm:w-1/2 min-w-80 p-4"
|
||||||
|
*ngFor="let deviceSummary of (deviceSummariesForHostGroup(hostId.value) | deviceSort:config.dashboard_sort:config.dashboard_display )"
|
||||||
|
[deviceWWN]="deviceSummary.device.wwn"
|
||||||
|
[deviceSummary]="deviceSummary"></app-dashboard-device>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {DashboardService} from 'app/modules/dashboard/dashboard.service';
|
|||||||
import {MatDialog} from '@angular/material/dialog';
|
import {MatDialog} from '@angular/material/dialog';
|
||||||
import {DashboardSettingsComponent} from 'app/layout/common/dashboard-settings/dashboard-settings.component';
|
import {DashboardSettingsComponent} from 'app/layout/common/dashboard-settings/dashboard-settings.component';
|
||||||
import {AppConfig} from 'app/core/config/app.config';
|
import {AppConfig} from 'app/core/config/app.config';
|
||||||
import {TreoConfigService} from '@treo/services/config';
|
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import {TemperaturePipe} from 'app/shared/temperature.pipe';
|
import {TemperaturePipe} from 'app/shared/temperature.pipe';
|
||||||
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
|
import {DeviceTitlePipe} from 'app/shared/device-title.pipe';
|
||||||
@@ -43,13 +43,13 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
|||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
* @param {DashboardService} _dashboardService
|
* @param {DashboardService} _dashboardService
|
||||||
* @param {TreoConfigService} _configService
|
* @param {ScrutinyConfigService} _configService
|
||||||
* @param {MatDialog} dialog
|
* @param {MatDialog} dialog
|
||||||
* @param {Router} router
|
* @param {Router} router
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private _dashboardService: DashboardService,
|
private _dashboardService: DashboardService,
|
||||||
private _configService: TreoConfigService,
|
private _configService: ScrutinyConfigService,
|
||||||
public dialog: MatDialog,
|
public dialog: MatDialog,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
)
|
)
|
||||||
@@ -150,7 +150,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceName = DeviceTitlePipe.deviceTitleWithFallback(deviceSummary.device, this.config.dashboardDisplay)
|
const deviceName = DeviceTitlePipe.deviceTitleWithFallback(deviceSummary.device, this.config.dashboard_display)
|
||||||
|
|
||||||
const deviceSeriesMetadata = {
|
const deviceSeriesMetadata = {
|
||||||
name: deviceName,
|
name: deviceName,
|
||||||
@@ -161,7 +161,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
|||||||
const newDate = new Date(tempHistory.date);
|
const newDate = new Date(tempHistory.date);
|
||||||
deviceSeriesMetadata.data.push({
|
deviceSeriesMetadata.data.push({
|
||||||
x: newDate,
|
x: newDate,
|
||||||
y: TemperaturePipe.formatTemperature(tempHistory.temp, this.config.temperatureUnit, false)
|
y: TemperaturePipe.formatTemperature(tempHistory.temp, this.config.temperature_unit, false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
deviceTemperatureSeries.push(deviceSeriesMetadata)
|
deviceTemperatureSeries.push(deviceSeriesMetadata)
|
||||||
@@ -212,7 +212,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
|||||||
y : {
|
y : {
|
||||||
|
|
||||||
formatter: (value) => {
|
formatter: (value) => {
|
||||||
return TemperaturePipe.formatTemperature(value, this.config.temperatureUnit, true) as string;
|
return TemperaturePipe.formatTemperature(value, this.config.temperature_unit, true) as string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<div class="flex items-center justify-between w-full my-4 px-4 xs:pr-0">
|
<div class="flex items-center justify-between w-full my-4 px-4 xs:pr-0">
|
||||||
<div class="mr-6">
|
<div class="mr-6">
|
||||||
<h2 class="m-0">Drive Details - {{device | deviceTitle:config.dashboardDisplay}} </h2>
|
<h2 class="m-0">Drive Details - {{device | deviceTitle:config.dashboard_display}} </h2>
|
||||||
<div class="text-secondary tracking-tight">Dive into S.M.A.R.T data</div>
|
<div class="text-secondary tracking-tight">Dive into S.M.A.R.T data</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Action buttons -->
|
<!-- Action buttons -->
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
<div class="text-secondary text-md">Powered On</div>
|
<div class="text-secondary text-md">Powered On</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-2 col-span-2 lt-md:col-span-1">
|
<div class="my-2 col-span-2 lt-md:col-span-1">
|
||||||
<div>{{smart_results[0]?.temp | temperature:config.temperatureUnit:true}}</div>
|
<div>{{smart_results[0]?.temp | temperature:config.temperature_unit:true}}</div>
|
||||||
<div class="text-secondary text-md">Temperature</div>
|
<div class="text-secondary text-md">Temperature</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {MatDialog} from '@angular/material/dialog';
|
|||||||
import {MatSort} from '@angular/material/sort';
|
import {MatSort} from '@angular/material/sort';
|
||||||
import {MatTableDataSource} from '@angular/material/table';
|
import {MatTableDataSource} from '@angular/material/table';
|
||||||
import {Subject} from 'rxjs';
|
import {Subject} from 'rxjs';
|
||||||
import {TreoConfigService} from '@treo/services/config';
|
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
|
||||||
import {animate, state, style, transition, trigger} from '@angular/animations';
|
import {animate, state, style, transition, trigger} from '@angular/animations';
|
||||||
import {formatDate} from '@angular/common';
|
import {formatDate} from '@angular/common';
|
||||||
import {takeUntil} from 'rxjs/operators';
|
import {takeUntil} from 'rxjs/operators';
|
||||||
@@ -44,13 +44,13 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
*
|
*
|
||||||
* @param {DetailService} _detailService
|
* @param {DetailService} _detailService
|
||||||
* @param {MatDialog} dialog
|
* @param {MatDialog} dialog
|
||||||
* @param {TreoConfigService} _configService
|
* @param {ScrutinyConfigService} _configService
|
||||||
* @param {string} locale
|
* @param {string} locale
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private _detailService: DetailService,
|
private _detailService: DetailService,
|
||||||
public dialog: MatDialog,
|
public dialog: MatDialog,
|
||||||
private _configService: TreoConfigService,
|
private _configService: ScrutinyConfigService,
|
||||||
@Inject(LOCALE_ID) public locale: string
|
@Inject(LOCALE_ID) public locale: string
|
||||||
) {
|
) {
|
||||||
// Set the private defaults
|
// Set the private defaults
|
||||||
|
|||||||
Reference in New Issue
Block a user