Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f935ceb48 | |||
| f844a435fd | |||
| 3a970e7a27 | |||
| 307c2bcdef | |||
| d62928aaae | |||
| d08a1e3ef6 | |||
| 2292041f9f | |||
| 75e4bf1d6e | |||
| 97add04276 | |||
| 1423f55d78 | |||
| 46d0b70399 | |||
| 168ca802d1 | |||
| 8c07e91f39 | |||
| 7979950c3b | |||
| 9846ba13e0 | |||
| 83839f7faf | |||
| 85fa3b1f8f | |||
| 4190f9a633 | |||
| 743ce27d2e | |||
| 399a2450ff | |||
| 934f16f0a5 | |||
| 0aeb13c181 | |||
| 5899bf2026 | |||
| 3b137964fc | |||
| 1bfdd0043f | |||
| 999c12748c | |||
| 6f283fd736 |
@@ -178,7 +178,9 @@ Scrutiny supports sending SMART device failure notifications via the following s
|
||||
- Telegram
|
||||
- Tulip
|
||||
|
||||
Check the `notify.urls` section of [example.scrutiny.yml](example.scrutiny.yaml) for more information and documentation for service specific setup.
|
||||
Check the `notify.urls` section of [example.scrutiny.yml](example.scrutiny.yaml) for examples.
|
||||
|
||||
For more information and troubleshooting, see the [TROUBLESHOOTING_NOTIFICATIONS.md](./docs/TROUBLESHOOTING_NOTIFICATIONS.md) file
|
||||
|
||||
### Testing Notifications
|
||||
|
||||
@@ -239,7 +241,7 @@ scrutiny-collector-metrics run --debug --log-file /tmp/collector.log
|
||||
| arm-6 | :white_check_mark: | |
|
||||
| arm-7 | :white_check_mark: | web/collector only. see [#236](https://github.com/AnalogJ/scrutiny/issues/236) |
|
||||
| arm64 | :white_check_mark: | :white_check_mark: |
|
||||
| freebsd | :white_check_mark: | |
|
||||
| freebsd | collector only. see [#238](https://github.com/AnalogJ/scrutiny/issues/238) | |
|
||||
| macos-amd64 | | :white_check_mark: |
|
||||
| macos-arm64 | | :white_check_mark: |
|
||||
| windows-amd64 | :white_check_mark: | |
|
||||
|
||||
@@ -13,7 +13,7 @@ func DevicePrefix() string {
|
||||
|
||||
func (d *Detect) Start() ([]models.Device, error) {
|
||||
d.Shell = shell.Create()
|
||||
// call the base/common functionality to get a list of devicess
|
||||
// call the base/common functionality to get a list of devices
|
||||
detectedDevices, err := d.SmartctlScan()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package detect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/common/shell"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/jaypipes/ghw"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -22,6 +25,7 @@ func (d *Detect) Start() ([]models.Device, error) {
|
||||
//inflate device info for detected devices.
|
||||
for ndx, _ := range detectedDevices {
|
||||
d.SmartCtlInfo(&detectedDevices[ndx]) //ignore errors.
|
||||
populateUdevInfo(&detectedDevices[ndx]) //ignore errors.
|
||||
}
|
||||
|
||||
return detectedDevices, nil
|
||||
@@ -49,3 +53,51 @@ func (d *Detect) wwnFallback(detectedDevice *models.Device) {
|
||||
//wwn must always be lowercase.
|
||||
detectedDevice.WWN = strings.ToLower(detectedDevice.WWN)
|
||||
}
|
||||
|
||||
// as discussed in
|
||||
// - https://github.com/AnalogJ/scrutiny/issues/225
|
||||
// - https://github.com/jaypipes/ghw/issues/59#issue-361915216
|
||||
// udev exposes its data in a standardized way under /run/udev/data/....
|
||||
func populateUdevInfo(detectedDevice *models.Device) error {
|
||||
// Get device major:minor numbers
|
||||
// `cat /sys/class/block/sda/dev`
|
||||
devNo, err := ioutil.ReadFile(filepath.Join("/sys/class/block/", detectedDevice.DeviceName, "dev"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Look up block device in udev runtime database
|
||||
// `cat /run/udev/data/b8:0`
|
||||
udevID := "b" + strings.TrimSpace(string(devNo))
|
||||
udevBytes, err := ioutil.ReadFile(filepath.Join("/run/udev/data/", udevID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deviceMountPaths := []string{}
|
||||
udevInfo := make(map[string]string)
|
||||
for _, udevLine := range strings.Split(string(udevBytes), "\n") {
|
||||
if strings.HasPrefix(udevLine, "E:") {
|
||||
if s := strings.SplitN(udevLine[2:], "=", 2); len(s) == 2 {
|
||||
udevInfo[s[0]] = s[1]
|
||||
}
|
||||
} else if strings.HasPrefix(udevLine, "S:") {
|
||||
deviceMountPaths = append(deviceMountPaths, udevLine[2:])
|
||||
}
|
||||
}
|
||||
|
||||
//Set additional device information.
|
||||
if deviceLabel, exists := udevInfo["ID_FS_LABEL"]; exists {
|
||||
detectedDevice.DeviceLabel = deviceLabel
|
||||
}
|
||||
if deviceUUID, exists := udevInfo["ID_FS_UUID"]; exists {
|
||||
detectedDevice.DeviceUUID = deviceUUID
|
||||
}
|
||||
if deviceSerialID, exists := udevInfo["ID_SERIAL"]; exists {
|
||||
detectedDevice.DeviceSerialID = fmt.Sprintf("%s-%s", udevInfo["ID_BUS"], deviceSerialID)
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package models
|
||||
|
||||
type Device struct {
|
||||
WWN string `json:"wwn"`
|
||||
HostId string `json:"host_id"`
|
||||
WWN string `json:"wwn"`
|
||||
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceUUID string `json:"device_uuid"`
|
||||
DeviceSerialID string `json:"device_serial_id"`
|
||||
DeviceLabel string `json:"device_label"`
|
||||
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
ModelName string `json:"model_name"`
|
||||
InterfaceType string `json:"interface_type"`
|
||||
@@ -17,6 +20,10 @@ type Device struct {
|
||||
SmartSupport bool `json:"smart_support"`
|
||||
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
|
||||
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.
|
||||
|
||||
// User provided metadata
|
||||
Label string `json:"label"`
|
||||
HostId string `json:"host_id"`
|
||||
}
|
||||
|
||||
type DeviceWrapper struct {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Notifications
|
||||
|
||||
As documented in [example.scrutiny.yaml](https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml#L59-L75)
|
||||
there are multiple ways to configure notifications for Scrutiny.
|
||||
|
||||
Under the hood we use a library called [Shoutrrr](https://github.com/containrrr/shoutrrr) to send our notifications, and you should use their documentation if you run into
|
||||
any issues: https://containrrr.dev/shoutrrr/services/overview/
|
||||
|
||||
|
||||
# Script Notifications
|
||||
|
||||
While the Shoutrrr library supports many popular providers for sending notifications Scrutiny also supports a "script" based
|
||||
notification system, allowing you to execute a custom script whenever a notification needs to be sent.
|
||||
Data is provided to this script using the following environmental variables:
|
||||
|
||||
```
|
||||
SCRUTINY_SUBJECT - eg. "Scrutiny SMART error (%s) detected on device: %s"
|
||||
SCRUTINY_DATE
|
||||
SCRUTINY_FAILURE_TYPE - EmailTest, SmartFail, ScrutinyFail
|
||||
SCRUTINY_DEVICE_NAME - eg. /dev/sda
|
||||
SCRUTINY_DEVICE_TYPE - ATA/SCSI/NVMe
|
||||
SCRUTINY_DEVICE_SERIAL - eg. WDDJ324KSO
|
||||
SCRUTINY_MESSAGE - eg. "Scrutiny SMART error notification for device: %s\nFailure Type: %s\nDevice Name: %s\nDevice Serial: %s\nDevice Type: %s\nDate: %s"
|
||||
```
|
||||
|
||||
@@ -40,6 +40,7 @@ web:
|
||||
# and store the information in the config file. If you 're re-using an existing influxdb installation, you'll need to provide
|
||||
# the `token`
|
||||
influxdb:
|
||||
# scheme: 'http'
|
||||
host: 0.0.0.0
|
||||
port: 8086
|
||||
# token: 'my-token'
|
||||
|
||||
@@ -39,6 +39,7 @@ func (c *configuration) Init() error {
|
||||
|
||||
c.SetDefault("notify.urls", []string{})
|
||||
|
||||
c.SetDefault("web.influxdb.scheme", "http")
|
||||
c.SetDefault("web.influxdb.host", "localhost")
|
||||
c.SetDefault("web.influxdb.port", "8086")
|
||||
c.SetDefault("web.influxdb.org", "scrutiny")
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Deprecated: m20220503120000.Device is deprecated, only used by db migrations
|
||||
type Device struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
CreatedAt time.Time
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package m20220509170100
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
|
||||
WWN string `json:"wwn" gorm:"primary_key"`
|
||||
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceUUID string `json:"device_uuid"`
|
||||
DeviceSerialID string `json:"device_serial_id"`
|
||||
DeviceLabel string `json:"device_label"`
|
||||
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
ModelName string `json:"model_name"`
|
||||
InterfaceType string `json:"interface_type"`
|
||||
InterfaceSpeed string `json:"interface_speed"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Firmware string `json:"firmware"`
|
||||
RotationSpeed int `json:"rotational_speed"`
|
||||
Capacity int64 `json:"capacity"`
|
||||
FormFactor string `json:"form_factor"`
|
||||
SmartSupport bool `json:"smart_support"`
|
||||
DeviceProtocol string `json:"device_protocol"` //protocol determines which smart attribute types are available (ATA, NVMe, SCSI)
|
||||
DeviceType string `json:"device_type"` //device type is used for querying with -d/t flag, should only be used by collector.
|
||||
|
||||
// User provided metadata
|
||||
Label string `json:"label"`
|
||||
HostId string `json:"host_id"`
|
||||
|
||||
// Data set by Scrutiny
|
||||
DeviceStatus pkg.DeviceStatus `json:"device_status"`
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func NewScrutinyRepository(appConfig config.Interface, globalLogger logrus.Field
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Create a new client using an InfluxDB server base URL and an authentication token
|
||||
influxdbUrl := fmt.Sprintf("http://%s:%s", appConfig.GetString("web.influxdb.host"), appConfig.GetString("web.influxdb.port"))
|
||||
influxdbUrl := fmt.Sprintf("%s://%s:%s", appConfig.GetString("web.influxdb.scheme"), appConfig.GetString("web.influxdb.host"), appConfig.GetString("web.influxdb.port"))
|
||||
globalLogger.Debugf("InfluxDB url: %s", influxdbUrl)
|
||||
|
||||
client := influxdb2.NewClient(influxdbUrl, appConfig.GetString("web.influxdb.token"))
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
func (sr *scrutinyRepository) RegisterDevice(ctx context.Context, dev models.Device) error {
|
||||
if err := sr.gormClient.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "wwn"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type"}),
|
||||
DoUpdates: clause.AssignmentColumns([]string{"host_id", "device_name", "device_type", "device_uuid", "device_serial_id", "device_label"}),
|
||||
}).Create(&dev).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,14 +2,18 @@ package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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/m20220509170100"
|
||||
"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/measurements"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/influxdata/influxdb-client-go/v2/api/http"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -22,9 +26,12 @@ import (
|
||||
|
||||
func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
|
||||
sr.logger.Infoln("Database migration starting")
|
||||
sr.logger.Infoln("Database migration starting. Please wait, this process may take a long time....")
|
||||
|
||||
m := gormigrate.New(sr.gormClient, gormigrate.DefaultOptions, []*gormigrate.Migration{
|
||||
gormMigrateOptions := gormigrate.DefaultOptions
|
||||
gormMigrateOptions.UseTransaction = true
|
||||
|
||||
m := gormigrate.New(sr.gormClient, gormMigrateOptions, []*gormigrate.Migration{
|
||||
{
|
||||
ID: "20201107210306", // v0.3.13 (pre-influxdb schema). 9fac3c6308dc6cb6cd5bbc43a68cd93e8fb20b87
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
@@ -38,16 +45,6 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
&m20201107210306.SmartNvmeAttribute{},
|
||||
)
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Migrator().DropTable(
|
||||
&m20201107210306.Device{},
|
||||
&m20201107210306.Smart{},
|
||||
&m20201107210306.SmartAtaAttribute{},
|
||||
&m20201107210306.SmartNvmeAttribute{},
|
||||
&m20201107210306.SmartNvmeAttribute{},
|
||||
"self_tests",
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "20220503113100", // backwards compatible - influxdb schema
|
||||
@@ -137,7 +134,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
smartTags,
|
||||
smartFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -147,7 +144,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
tempTags,
|
||||
tempFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -166,7 +163,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
smartFields,
|
||||
postSmartResults.Date, ctx)
|
||||
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -176,7 +173,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
tempTags,
|
||||
tempFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -194,7 +191,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
smartTags,
|
||||
smartFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -204,7 +201,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
tempTags,
|
||||
tempFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -221,7 +218,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
smartTags,
|
||||
smartFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -231,7 +228,7 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
tempTags,
|
||||
tempFields,
|
||||
postSmartResults.Date, ctx)
|
||||
if err != nil {
|
||||
if ignorePastRetentionPolicyError(err) != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -257,20 +254,44 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
//migrate the device database to the current version
|
||||
//migrate the device database
|
||||
return tx.AutoMigrate(m20220503120000.Device{})
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "m20220509170100", // addl udev device data
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
|
||||
//migrate the device database.
|
||||
// adding addl columns (device_label, device_uuid, device_serial_id)
|
||||
return tx.AutoMigrate(m20220509170100.Device{})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err := m.Migrate(); err != nil {
|
||||
sr.logger.Errorf("Database migration failed with error: %w", err)
|
||||
sr.logger.Errorf("Database migration failed with error. \n Please open a github issue at https://github.com/AnalogJ/scrutiny and attach a copy of your scrutiny.db file. \n %v", err)
|
||||
return err
|
||||
}
|
||||
sr.logger.Infoln("Database migration completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
//When adding data to influxdb, an error may be returned if the data point is outside the range of the retention policy.
|
||||
//This function will ignore retention policy errors, and allow the migration to continue.
|
||||
func ignorePastRetentionPolicyError(err error) error {
|
||||
var influxDbWriteError *http.Error
|
||||
if errors.As(err, &influxDbWriteError) {
|
||||
if influxDbWriteError.StatusCode == 422 {
|
||||
log.Infoln("ignoring error: attempted to writePoint past retention period duration")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
func m20201107210306_FromPreInfluxDBTempCreatePostInfluxDBTemp(preDevice m20201107210306.Device, preSmartResult m20201107210306.Smart) (error, measurements.SmartTemperature) {
|
||||
//extract temperature data for every datapoint
|
||||
|
||||
@@ -21,6 +21,10 @@ type Device struct {
|
||||
WWN string `json:"wwn" gorm:"primary_key"`
|
||||
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceUUID string `json:"device_uuid"`
|
||||
DeviceSerialID string `json:"device_serial_id"`
|
||||
DeviceLabel string `json:"device_label"`
|
||||
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
ModelName string `json:"model_name"`
|
||||
InterfaceType string `json:"interface_type"`
|
||||
|
||||
@@ -171,7 +171,7 @@ func (sm *Smart) ProcessNvmeSmartInfo(nvmeSmartHealthInformationLog collector.Nv
|
||||
"power_on_hours": (&SmartNvmeAttribute{AttributeId: "power_on_hours", Value: nvmeSmartHealthInformationLog.PowerOnHours, Threshold: -1}).PopulateAttributeStatus(),
|
||||
"unsafe_shutdowns": (&SmartNvmeAttribute{AttributeId: "unsafe_shutdowns", Value: nvmeSmartHealthInformationLog.UnsafeShutdowns, Threshold: -1}).PopulateAttributeStatus(),
|
||||
"media_errors": (&SmartNvmeAttribute{AttributeId: "media_errors", Value: nvmeSmartHealthInformationLog.MediaErrors, Threshold: 0}).PopulateAttributeStatus(),
|
||||
"num_err_log_entries": (&SmartNvmeAttribute{AttributeId: "num_err_log_entries", Value: nvmeSmartHealthInformationLog.NumErrLogEntries, Threshold: 0}).PopulateAttributeStatus(),
|
||||
"num_err_log_entries": (&SmartNvmeAttribute{AttributeId: "num_err_log_entries", Value: nvmeSmartHealthInformationLog.NumErrLogEntries, Threshold: -1}).PopulateAttributeStatus(),
|
||||
"warning_temp_time": (&SmartNvmeAttribute{AttributeId: "warning_temp_time", Value: nvmeSmartHealthInformationLog.WarningTempTime, Threshold: -1}).PopulateAttributeStatus(),
|
||||
"critical_comp_time": (&SmartNvmeAttribute{AttributeId: "critical_comp_time", Value: nvmeSmartHealthInformationLog.CriticalCompTime, Threshold: -1}).PopulateAttributeStatus(),
|
||||
}
|
||||
|
||||
@@ -445,50 +445,6 @@ var AtaMetadata = map[int]AtaAttributeMetadata{
|
||||
Ideal: ObservedThresholdIdealLow,
|
||||
Critical: false,
|
||||
Description: "This attribute indicates the count of full hard disk power on/off cycles.",
|
||||
ObservedThresholds: []ObservedThreshold{
|
||||
{
|
||||
Low: 0,
|
||||
High: 13,
|
||||
AnnualFailureRate: 0.019835987118930823,
|
||||
ErrorInterval: []float64{0.016560870164523494, 0.023569242386797896},
|
||||
},
|
||||
{
|
||||
Low: 13,
|
||||
High: 26,
|
||||
AnnualFailureRate: 0.038210930067894826,
|
||||
ErrorInterval: []float64{0.03353859179329295, 0.0433520775718649},
|
||||
},
|
||||
{
|
||||
Low: 26,
|
||||
High: 39,
|
||||
AnnualFailureRate: 0.11053528307302571,
|
||||
ErrorInterval: []float64{0.09671061589521368, 0.1257816678419765},
|
||||
},
|
||||
{
|
||||
Low: 39,
|
||||
High: 52,
|
||||
AnnualFailureRate: 0.16831189443375036,
|
||||
ErrorInterval: []float64{0.1440976510675928, 0.19543066007594895},
|
||||
},
|
||||
{
|
||||
Low: 52,
|
||||
High: 65,
|
||||
AnnualFailureRate: 0.20630344262550107,
|
||||
ErrorInterval: []float64{0.1693965932069108, 0.2488633537247856},
|
||||
},
|
||||
{
|
||||
Low: 65,
|
||||
High: 78,
|
||||
AnnualFailureRate: 0.1030972634140512,
|
||||
ErrorInterval: []float64{0.06734655535304743, 0.15106137807407605},
|
||||
},
|
||||
{
|
||||
Low: 78,
|
||||
High: 91,
|
||||
AnnualFailureRate: 0.12354840389522469,
|
||||
ErrorInterval: []float64{0.06578432170016109, 0.21127153335749593},
|
||||
},
|
||||
},
|
||||
},
|
||||
13: {
|
||||
ID: 13,
|
||||
|
||||
@@ -2,4 +2,4 @@ package version
|
||||
|
||||
// VERSION is the app-global version string, which will be replaced with a
|
||||
// new value during packaging
|
||||
const VERSION = "0.4.6"
|
||||
const VERSION = "0.4.7"
|
||||
|
||||
@@ -93,6 +93,7 @@ func (suite *ServerTestSuite) TestHealthRoute() {
|
||||
fakeConfig.EXPECT().GetString("web.src.frontend.path").Return(parentPath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -132,6 +133,7 @@ func (suite *ServerTestSuite) TestRegisterDevicesRoute() {
|
||||
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.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -171,6 +173,7 @@ func (suite *ServerTestSuite) TestUploadDeviceMetricsRoute() {
|
||||
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.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -219,6 +222,7 @@ func (suite *ServerTestSuite) TestPopulateMultiple() {
|
||||
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.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -314,6 +318,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_WebhookFailure() {
|
||||
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.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -352,6 +357,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptFailure() {
|
||||
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.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -390,6 +396,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ScriptSuccess() {
|
||||
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.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -428,6 +435,7 @@ func (suite *ServerTestSuite) TestSendTestNotificationRoute_ShoutrrrFailure() {
|
||||
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.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -465,6 +473,7 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
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.listen.basepath").Return(suite.Basepath).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.scheme").Return("http").AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.port").Return("8086").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("web.influxdb.token").Return(true).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("web.influxdb.token").Return("my-super-secret-auth-token").AnyTimes()
|
||||
@@ -509,5 +518,5 @@ func (suite *ServerTestSuite) TestGetDevicesSummaryRoute_Nvme() {
|
||||
|
||||
//assert
|
||||
require.Equal(suite.T(), "a4c8e8ed-11a0-4c97-9bba-306440f1b944", deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.WWN)
|
||||
require.Equal(suite.T(), pkg.DeviceStatusFailedScrutiny, deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.DeviceStatus)
|
||||
require.Equal(suite.T(), pkg.DeviceStatusPassed, deviceSummary.Data.Summary["a4c8e8ed-11a0-4c97-9bba-306440f1b944"].Device.DeviceStatus)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import * as _ from 'lodash';
|
||||
import { TREO_APP_CONFIG } from '@treo/services/config/config.constants';
|
||||
|
||||
const SCRUTINY_CONFIG_LOCAL_STORAGE_KEY = 'scrutiny';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
@@ -10,14 +12,22 @@ export class TreoConfigService
|
||||
{
|
||||
// Private
|
||||
private _config: BehaviorSubject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor(@Inject(TREO_APP_CONFIG) config: any)
|
||||
constructor(@Inject(TREO_APP_CONFIG) defaultConfig: any)
|
||||
{
|
||||
let currentScrutinyConfig = defaultConfig
|
||||
|
||||
let localConfigStr = localStorage.getItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY)
|
||||
if(localConfigStr){
|
||||
//check localstorage for a value
|
||||
let localConfig = JSON.parse(localConfigStr)
|
||||
currentScrutinyConfig = localConfig
|
||||
}
|
||||
|
||||
// Set the private defaults
|
||||
this._config = new BehaviorSubject(config);
|
||||
this._config = new BehaviorSubject(currentScrutinyConfig);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@@ -27,15 +37,20 @@ export class TreoConfigService
|
||||
/**
|
||||
* Setter and getter for config
|
||||
*/
|
||||
//Setter
|
||||
set config(value: any)
|
||||
{
|
||||
// Merge the new config over to the current config
|
||||
const config = _.merge({}, this._config.getValue(), value);
|
||||
|
||||
//Store the config in localstorage
|
||||
localStorage.setItem(SCRUTINY_CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config));
|
||||
|
||||
// Execute the observable
|
||||
this._config.next(config);
|
||||
}
|
||||
|
||||
//Getter
|
||||
get config$(): Observable<any>
|
||||
{
|
||||
return this._config.asObservable();
|
||||
|
||||
@@ -11,6 +11,11 @@ export interface AppConfig
|
||||
{
|
||||
theme: Theme;
|
||||
layout: Layout;
|
||||
|
||||
// Dashboard options
|
||||
dashboardDisplay: string;
|
||||
dashboardSort: string;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,6 +28,9 @@ export interface AppConfig
|
||||
*/
|
||||
export const appConfig: AppConfig = {
|
||||
theme : "light",
|
||||
layout: "material"
|
||||
layout: "material",
|
||||
|
||||
dashboardDisplay: "name",
|
||||
dashboardSort: "status",
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ export const summary = {
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x5000c500673e6b5f",
|
||||
"device_name": "sdg",
|
||||
"device_label": "14TB-WD-DRIVE2",
|
||||
"device_uuid": "",
|
||||
"device_serial_id": "ata-ST6000DX000-1H217Z-Z4DXXXXX",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "ST6000DX000-1H217Z",
|
||||
"interface_type": "SCSI",
|
||||
@@ -35,6 +38,9 @@ export const summary = {
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x5000cca252c859cc",
|
||||
"device_name": "sdd",
|
||||
"device_label": "14TB-WD-DRIVE1",
|
||||
"device_uuid": "806cf4bc-d160-4d96-8ee9-3ab7cf2a2e1f",
|
||||
"device_serial_id": "ata-WDC_WD80EFAX-68LHPN0-7SGLXXXXX",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC_WD80EFAX-68LHPN0",
|
||||
"interface_type": "SCSI",
|
||||
@@ -68,6 +74,9 @@ export const summary = {
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x5000cca264eb01d7",
|
||||
"device_name": "sdb",
|
||||
"device_label": "14TB-WD-DRIVE5",
|
||||
"device_uuid": "8125ec6d-a7e4-4950-ac84-72d6a4d67128",
|
||||
"device_serial_id": "ata-WDC_WD140EDFZ-11A0VA0-9RK1XXXXX",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC_WD140EDFZ-11A0VA0",
|
||||
"interface_type": "SCSI",
|
||||
@@ -101,6 +110,9 @@ export const summary = {
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x5000cca264ebc248",
|
||||
"device_name": "sde",
|
||||
"device_label": "14TB-WD-DRIVE3",
|
||||
"device_uuid": "9eb60cde-d6d0-4172-b520-b241a6a5477f",
|
||||
"device_serial_id": "ata-WDC_WD140EDFZ-11A0VA0-9RK3XXXXX",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC_WD140EDFZ-11A0VA0",
|
||||
"interface_type": "SCSI",
|
||||
@@ -125,6 +137,9 @@ export const summary = {
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x5000cca264ec3183",
|
||||
"device_name": "sdc",
|
||||
"device_label": "14TB-WD-DRIVE6",
|
||||
"device_uuid": "e1378723-7861-49b9-8e01-0bd063f0ecdd",
|
||||
"device_serial_id": "ata-WDC_WD140EDFZ-11A0VA0-9RK4XXXXX",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC_WD140EDFZ-11A0VA0",
|
||||
"interface_type": "SCSI",
|
||||
@@ -138,7 +153,7 @@ export const summary = {
|
||||
"device_protocol": "",
|
||||
"device_type": "",
|
||||
"label": "",
|
||||
"host_id": "",
|
||||
"host_id": "custom host id",
|
||||
"device_status": 1
|
||||
},
|
||||
"smart": {
|
||||
@@ -542,6 +557,9 @@ export const summary = {
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x50014ee20b2a72a9",
|
||||
"device_name": "sdf",
|
||||
"device_label": "8.0TB-WD-4",
|
||||
"device_uuid": "fc684dcc-aa2f-44f3-a958-d302dc7dd46d",
|
||||
"device_serial_id": "ata-WDC_WD60EFRX-68MYMN1-WXL1HXXXXX",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "WDC_WD60EFRX-68MYMN1",
|
||||
"interface_type": "SCSI",
|
||||
@@ -566,6 +584,9 @@ export const summary = {
|
||||
"DeletedAt": null,
|
||||
"wwn": "0x5002538e40a22954",
|
||||
"device_name": "sda",
|
||||
"device_label": "",
|
||||
"device_uuid": "",
|
||||
"device_serial_id": "ata-Samsung_SSD_860_EVO_500GB-S3YZNB0KBXXXXXX",
|
||||
"manufacturer": "ATA",
|
||||
"model_name": "Samsung_SSD_860_EVO_500GB",
|
||||
"interface_type": "SCSI",
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<div [ngClass]="{ 'border-green': deviceSummary.device.device_status == 0 && deviceSummary.smart,
|
||||
'border-red': deviceSummary.device.device_status != 0 }"
|
||||
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">
|
||||
<mat-icon class="icon-size-96 opacity-12 text-green"
|
||||
*ngIf="deviceSummary.device.device_status == 0 && deviceSummary.smart"
|
||||
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
|
||||
<mat-icon class="icon-size-96 opacity-12 text-red"
|
||||
*ngIf="deviceSummary.device.device_status != 0"
|
||||
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
|
||||
<mat-icon class="icon-size-96 opacity-12 text-yellow"
|
||||
*ngIf="!deviceSummary.smart"
|
||||
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-col">
|
||||
<a [routerLink]="'/device/'+ deviceSummary.device.wwn"
|
||||
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceTitle(deviceSummary.device)}}</a>
|
||||
<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' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto" *ngIf="deviceSummary.device">
|
||||
<button mat-icon-button
|
||||
[matMenuTriggerFor]="previousStatementMenu">
|
||||
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #previousStatementMenu="matMenu">
|
||||
<a mat-menu-item [routerLink]="'/device/'+ deviceSummary.device.wwn">
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="icon-size-20 mr-3"
|
||||
[svgIcon]="'payment'"></mat-icon>
|
||||
<span>View Details</span>
|
||||
</span>
|
||||
</a>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
<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="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>
|
||||
<ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
|
||||
</div>
|
||||
<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="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.collector_date; else unknownTemp">{{ deviceSummary.smart?.temp }}°C</div>
|
||||
<ng-template #unknownTemp><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
|
||||
</div>
|
||||
<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">Capacity</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none">{{ deviceSummary.device.capacity | fileSize}}</div>
|
||||
</div>
|
||||
<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">Powered On</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="deviceSummary.smart?.power_on_hours; else unknownPoweredOn">{{ humanizeDuration(deviceSummary.smart?.power_on_hours * 60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] }) }}</div>
|
||||
<ng-template #unknownPoweredOn><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DashboardDeviceComponent } from './dashboard-device.component';
|
||||
|
||||
describe('DashboardDeviceComponent', () => {
|
||||
let component: DashboardDeviceComponent;
|
||||
let fixture: ComponentFixture<DashboardDeviceComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DashboardDeviceComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DashboardDeviceComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import * as moment from "moment";
|
||||
import {takeUntil} from "rxjs/operators";
|
||||
import {AppConfig} from "app/core/config/app.config";
|
||||
import {TreoConfigService} from "@treo/services/config";
|
||||
import {Subject} from "rxjs";
|
||||
import humanizeDuration from 'humanize-duration'
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard-device',
|
||||
templateUrl: './dashboard-device.component.html',
|
||||
styleUrls: ['./dashboard-device.component.scss']
|
||||
})
|
||||
export class DashboardDeviceComponent implements OnInit {
|
||||
@Input() deviceSummary: any;
|
||||
@Input() deviceWWN: string;
|
||||
|
||||
config: AppConfig;
|
||||
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
constructor(
|
||||
private _configService: TreoConfigService,
|
||||
) {
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to config changes
|
||||
this._configService.config$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((config: AppConfig) => {
|
||||
this.config = config;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
classDeviceLastUpdatedOn(deviceSummary){
|
||||
if (deviceSummary.device.device_status !== 0) {
|
||||
return 'text-red' // if the device has failed, always highlight in red
|
||||
} else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){
|
||||
if(moment().subtract(14, 'd').isBefore(deviceSummary.smart.collector_date)){
|
||||
// this device was updated in the last 2 weeks.
|
||||
return 'text-green'
|
||||
} else if(moment().subtract(1, 'm').isBefore(deviceSummary.smart.collector_date)){
|
||||
// this device was updated in the last month
|
||||
return 'text-yellow'
|
||||
} else{
|
||||
// last updated more than a month ago.
|
||||
return 'text-red'
|
||||
}
|
||||
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
deviceTitle(disk){
|
||||
|
||||
console.log(`Displaying Device ${disk.wwn} with: ${this.config.dashboardDisplay}`)
|
||||
let titleParts = []
|
||||
if (disk.host_id) titleParts.push(disk.host_id)
|
||||
|
||||
//add device identifier (fallback to generated device name)
|
||||
titleParts.push(deviceDisplayTitle(disk, this.config.dashboardDisplay) || deviceDisplayTitle(disk, 'name'))
|
||||
|
||||
return titleParts.join(' - ')
|
||||
}
|
||||
|
||||
deviceStatusString(deviceStatus){
|
||||
if(deviceStatus == 0){
|
||||
return "passed"
|
||||
} else {
|
||||
return "failed"
|
||||
}
|
||||
}
|
||||
|
||||
readonly humanizeDuration = humanizeDuration;
|
||||
|
||||
}
|
||||
|
||||
export function deviceDisplayTitle(disk, titleType: string){
|
||||
let titleParts = []
|
||||
switch(titleType){
|
||||
case 'name':
|
||||
titleParts.push(`/dev/${disk.device_name}`)
|
||||
if (disk.device_type && disk.device_type != 'scsi' && disk.device_type != 'ata'){
|
||||
titleParts.push(disk.device_type)
|
||||
}
|
||||
titleParts.push(disk.model_name)
|
||||
|
||||
break;
|
||||
case 'serial_id':
|
||||
if(!disk.device_serial_id) return ''
|
||||
titleParts.push(`/by-id/${disk.device_serial_id}`)
|
||||
break;
|
||||
case 'uuid':
|
||||
if(!disk.device_uuid) return ''
|
||||
titleParts.push(`/by-uuid/${disk.device_uuid}`)
|
||||
break;
|
||||
case 'label':
|
||||
if(disk.label){
|
||||
titleParts.push(disk.label)
|
||||
} else if(disk.device_label){
|
||||
titleParts.push(`/by-label/${disk.device_label}`)
|
||||
}
|
||||
break;
|
||||
}
|
||||
return titleParts.join(' - ')
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { Overlay } from '@angular/cdk/overlay';
|
||||
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import {DashboardDeviceComponent} from 'app/layout/common/dashboard-device/dashboard-device.component'
|
||||
import { MatDialogModule } from "@angular/material/dialog";
|
||||
import { MatButtonToggleModule} from "@angular/material/button-toggle";
|
||||
import {MatTabsModule} from "@angular/material/tabs";
|
||||
import {MatSliderModule} from "@angular/material/slider";
|
||||
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
|
||||
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||
import {dashboardRoutes} from "../../../modules/dashboard/dashboard.routing";
|
||||
import {MatDividerModule} from "@angular/material/divider";
|
||||
import {MatMenuModule} from "@angular/material/menu";
|
||||
import {MatProgressBarModule} from "@angular/material/progress-bar";
|
||||
import {MatSortModule} from "@angular/material/sort";
|
||||
import {MatTableModule} from "@angular/material/table";
|
||||
import {NgApexchartsModule} from "ng-apexcharts";
|
||||
import {DashboardSettingsModule} from "../dashboard-settings/dashboard-settings.module";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DashboardDeviceComponent
|
||||
],
|
||||
imports : [
|
||||
RouterModule.forChild([]),
|
||||
RouterModule.forChild(dashboardRoutes),
|
||||
MatButtonModule,
|
||||
MatDividerModule,
|
||||
MatTooltipModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
MatProgressBarModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
NgApexchartsModule,
|
||||
SharedModule,
|
||||
],
|
||||
exports : [
|
||||
DashboardDeviceComponent,
|
||||
],
|
||||
providers : []
|
||||
})
|
||||
export class DashboardDeviceModule
|
||||
{
|
||||
}
|
||||
+30
-21
@@ -1,14 +1,23 @@
|
||||
<h2 mat-dialog-title>Scrutiny Settings</h2>
|
||||
<mat-dialog-content class="mat-typography">
|
||||
|
||||
<form class="flex flex-col p-8 pb-0 overflow-hidden">
|
||||
<div class="flex flex-col gt-xs:flex-row">
|
||||
<mat-form-field class="flex-auto gt-xs:pr-3">
|
||||
<div class="flex flex-col p-8 pb-0 overflow-hidden">
|
||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
|
||||
<mat-label>Display Title</mat-label>
|
||||
<mat-select [(ngModel)]="dashboardDisplay">
|
||||
<mat-option value="name">Name</mat-option>
|
||||
<mat-option value="serial_id">Serial ID</mat-option>
|
||||
<mat-option value="uuid">UUID</mat-option>
|
||||
<mat-option value="label">Label</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pl-3">
|
||||
<mat-label>Sort By</mat-label>
|
||||
<mat-select [value]="'status'">
|
||||
<mat-option value="status">Status</mat-option>
|
||||
<mat-option value="name" disabled>Name</mat-option>
|
||||
<mat-option value="label" disabled>Label</mat-option>
|
||||
<mat-select [(ngModel)]="dashboardSort">
|
||||
<mat-option value="status">Status</mat-option>
|
||||
<mat-option value="title">Title</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@@ -17,61 +26,61 @@
|
||||
<mat-tab-group mat-align-tabs="start">
|
||||
<mat-tab label="Ata">
|
||||
|
||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<div matTooltip="not yet implemented" class="gray-200 flex flex-col mt-5 gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-md:pr-3">
|
||||
<mat-label>Critical Error Threshold</mat-label>
|
||||
<input matInput [value]="'10%'">
|
||||
<input disabled matInput [value]="'10%'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto gt-md:pl-3">
|
||||
<mat-label>Critical Warning Threshold</mat-label>
|
||||
<input matInput>
|
||||
<input disabled matInput>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gt-md:flex-row">
|
||||
<div matTooltip="not yet implemented" class="gray-200 flex flex-col gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-md:pr-3">
|
||||
<mat-label>Error Threshold</mat-label>
|
||||
<input matInput [value]="'20%'">
|
||||
<input disabled matInput [value]="'20%'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto gt-md:pl-3">
|
||||
<mat-label>Warning Threshold</mat-label>
|
||||
<input matInput [value]="'10%'">
|
||||
<input disabled matInput [value]="'10%'">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
</mat-tab>
|
||||
<mat-tab label="NVMe">
|
||||
|
||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<div matTooltip="not yet implemented" class="gray-200 flex flex-col mt-5 gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-md:pr-3">
|
||||
<mat-label>Critical Error Threshold</mat-label>
|
||||
<input matInput [value]="'enabled'">
|
||||
<input disabled matInput [value]="'enabled'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto gt-md:pl-3">
|
||||
<mat-label>Critical Warning Threshold</mat-label>
|
||||
<input matInput>
|
||||
<input disabled matInput>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
</mat-tab>
|
||||
<mat-tab label="SCSI">
|
||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||
<div matTooltip="not yet implemented" class="gray-200 flex flex-col mt-5 gt-md:flex-row">
|
||||
<mat-form-field class="flex-auto gt-md:pr-3">
|
||||
<mat-label>Critical Error Threshold</mat-label>
|
||||
<input matInput [value]="'enabled'">
|
||||
<input disabled matInput [value]="'enabled'">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto gt-md:pl-3">
|
||||
<mat-label>Critical Warning Threshold</mat-label>
|
||||
<input matInput>
|
||||
<input disabled matInput>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<button mat-button matTooltip="not yet implemented" [mat-dialog-close]="true" cdkFocusInitial>Save</button>
|
||||
<button mat-button mat-dialog-close (click)="saveSettings()" cdkFocusInitial>Save</button>
|
||||
</mat-dialog-actions>
|
||||
|
||||
+36
-1
@@ -1,4 +1,8 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import {AppConfig} from 'app/core/config/app.config';
|
||||
import { TreoConfigService } from '@treo/services/config';
|
||||
import {Subject} from "rxjs";
|
||||
import {takeUntil} from "rxjs/operators";
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard-settings',
|
||||
@@ -7,10 +11,41 @@ import { Component, OnInit } from '@angular/core';
|
||||
})
|
||||
export class DashboardSettingsComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
dashboardDisplay: string;
|
||||
dashboardSort: string;
|
||||
|
||||
// Private
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
constructor(
|
||||
private _configService: TreoConfigService,
|
||||
) {
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to config changes
|
||||
this._configService.config$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((config: AppConfig) => {
|
||||
|
||||
// Store the config
|
||||
this.dashboardDisplay = config.dashboardDisplay;
|
||||
this.dashboardSort = config.dashboardSort;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
saveSettings(): void {
|
||||
var newSettings = {
|
||||
dashboardDisplay: this.dashboardDisplay,
|
||||
dashboardSort: this.dashboardSort
|
||||
}
|
||||
this._configService.config = newSettings
|
||||
console.log(`Saved Settings: ${JSON.stringify(newSettings)}`)
|
||||
}
|
||||
|
||||
formatLabel(value: number) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -47,71 +47,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap w-full">
|
||||
<div *ngFor="let summary of data.data.summary | keyvalue" class="flex gt-sm:w-1/2 min-w-80 p-4">
|
||||
<div [ngClass]="{ 'border-green': summary.value.device.device_status == 0 && summary.value.smart,
|
||||
'border-red': summary.value.device.device_status != 0 }"
|
||||
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">
|
||||
<mat-icon class="icon-size-96 opacity-12 text-green"
|
||||
*ngIf="summary.value.device.device_status == 0 && summary.value.smart"
|
||||
[svgIcon]="'heroicons_outline:check-circle'"></mat-icon>
|
||||
<mat-icon class="icon-size-96 opacity-12 text-red"
|
||||
*ngIf="summary.value.device.device_status != 0"
|
||||
[svgIcon]="'heroicons_outline:exclamation-circle'"></mat-icon>
|
||||
<mat-icon class="icon-size-96 opacity-12 text-yellow"
|
||||
*ngIf="!summary.value.smart"
|
||||
[svgIcon]="'heroicons_outline:question-mark-circle'"></mat-icon>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-col">
|
||||
<a [routerLink]="'/device/'+ summary.value.device.wwn"
|
||||
class="font-bold text-md text-secondary uppercase tracking-wider">{{deviceTitle(summary.value.device)}}</a>
|
||||
<div [ngClass]="classDeviceLastUpdatedOn(summary.value)" class="font-medium text-sm" *ngIf="summary.value.smart">
|
||||
Last Updated on {{summary.value.smart.collector_date | date:'MMMM dd, yyyy - HH:mm' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto" *ngIf="summary.value.device">
|
||||
<button mat-icon-button
|
||||
[matMenuTriggerFor]="previousStatementMenu">
|
||||
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #previousStatementMenu="matMenu">
|
||||
<a mat-menu-item [routerLink]="'/device/'+ summary.value.device.wwn">
|
||||
<span class="flex items-center">
|
||||
<mat-icon class="icon-size-20 mr-3"
|
||||
[svgIcon]="'payment'"></mat-icon>
|
||||
<span>View Details</span>
|
||||
</span>
|
||||
</a>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
<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="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Status</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.collector_date; else unknownStatus">{{ deviceStatusString(summary.value.device.device_status) | titlecase}}</div>
|
||||
<ng-template #unknownStatus><div class="mt-2 font-medium text-3xl leading-none">No Data</div></ng-template>
|
||||
</div>
|
||||
<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="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.collector_date; else unknownTemp">{{ summary.value.smart?.temp }}°C</div>
|
||||
<ng-template #unknownTemp><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
|
||||
</div>
|
||||
<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">Capacity</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none">{{ summary.value.device.capacity | fileSize}}</div>
|
||||
</div>
|
||||
<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">Powered On</div>
|
||||
<div class="mt-2 font-medium text-3xl leading-none" *ngIf="summary.value.smart?.power_on_hours; else unknownPoweredOn">{{ humanizeDuration(summary.value.smart?.power_on_hours * 60 * 60 * 1000, { round: true, largest: 1, units: ['y', 'd', 'h'] }) }}</div>
|
||||
<ng-template #unknownPoweredOn><div class="mt-2 font-medium text-3xl leading-none">--</div></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap w-full" *ngFor="let hostId of hostGroups | keyvalue">
|
||||
<h3 class="ml-4" *ngIf="hostId.key">{{hostId.key}}</h3>
|
||||
<div class="flex flex-wrap w-full">
|
||||
<app-dashboard-device 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Drive Temperatures -->
|
||||
<div class="flex flex-auto w-full min-w-80 h-90 p-4">
|
||||
<div class="flex flex-col flex-auto bg-card shadow-md rounded overflow-hidden">
|
||||
@@ -123,22 +67,22 @@
|
||||
</div>
|
||||
<div>
|
||||
<button class="h-8 min-h-8 px-2"
|
||||
matTooltip="not yet implemented"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="tempRangeMenu">
|
||||
<span class="font-medium text-sm text-hint">1 week</span>
|
||||
<span class="font-medium text-sm text-hint">{{tempDurationKey}}</span>
|
||||
</button>
|
||||
<mat-menu #tempRangeMenu="matMenu">
|
||||
<button mat-menu-item>1 month</button>
|
||||
<button mat-menu-item>12 months</button>
|
||||
<button mat-menu-item>all time</button>
|
||||
<button (click)="changeSummaryTempDuration('forever')" mat-menu-item>forever</button>
|
||||
<button (click)="changeSummaryTempDuration('year')" mat-menu-item>year</button>
|
||||
<button (click)="changeSummaryTempDuration('month')" mat-menu-item>month</button>
|
||||
<button (click)="changeSummaryTempDuration('week')" mat-menu-item>week</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex flex-col flex-auto">
|
||||
<apx-chart *ngIf="temperatureOptions" class="flex-auto w-full h-full"
|
||||
<apx-chart #tempChart *ngIf="temperatureOptions" class="flex-auto w-full h-full"
|
||||
[chart]="temperatureOptions.chart"
|
||||
[colors]="temperatureOptions.colors"
|
||||
[fill]="temperatureOptions.fill"
|
||||
|
||||
@@ -3,12 +3,14 @@ import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { ApexOptions } from 'ng-apexcharts';
|
||||
import {ApexOptions, ChartComponent} from 'ng-apexcharts';
|
||||
import { DashboardService } from 'app/modules/dashboard/dashboard.service';
|
||||
import * as moment from "moment";
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import { DashboardSettingsComponent } from 'app/layout/common/dashboard-settings/dashboard-settings.component';
|
||||
import humanizeDuration from 'humanize-duration'
|
||||
import {deviceDisplayTitle} from "app/layout/common/dashboard-device/dashboard-device.component";
|
||||
import {AppConfig} from "app/core/config/app.config";
|
||||
import {TreoConfigService} from "@treo/services/config";
|
||||
import {Router} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector : 'example',
|
||||
@@ -20,10 +22,14 @@ import humanizeDuration from 'humanize-duration'
|
||||
export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
{
|
||||
data: any;
|
||||
hostGroups: { [hostId: string]: string[] } = {}
|
||||
temperatureOptions: ApexOptions;
|
||||
tempDurationKey: string = "forever"
|
||||
config: AppConfig;
|
||||
|
||||
// Private
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
@ViewChild("tempChart", { static: false }) tempChart: ChartComponent;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -32,7 +38,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
*/
|
||||
constructor(
|
||||
private _smartService: DashboardService,
|
||||
public dialog: MatDialog
|
||||
private _configService: TreoConfigService,
|
||||
public dialog: MatDialog,
|
||||
private router: Router,
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
@@ -49,6 +57,28 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
|
||||
// Subscribe to config changes
|
||||
this._configService.config$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((config: AppConfig) => {
|
||||
|
||||
//check if the old config and the new config do not match.
|
||||
let oldConfig = JSON.stringify(this.config)
|
||||
let newConfig = JSON.stringify(config)
|
||||
|
||||
if(oldConfig != newConfig){
|
||||
console.log(`Configuration updated: ${newConfig} vs ${oldConfig}`)
|
||||
// Store the config
|
||||
this.config = config;
|
||||
|
||||
if(oldConfig){
|
||||
console.log("reloading component...")
|
||||
this.refreshComponent()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Get the data
|
||||
this._smartService.data$
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
@@ -57,6 +87,15 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
// Store the data
|
||||
this.data = data;
|
||||
|
||||
//generate group data.
|
||||
for(let wwn in this.data.data.summary){
|
||||
let hostid = this.data.data.summary[wwn].device.host_id
|
||||
let hostDeviceList = this.hostGroups[hostid] || []
|
||||
hostDeviceList.push(wwn)
|
||||
this.hostGroups[hostid] = hostDeviceList
|
||||
}
|
||||
console.log(this.hostGroups)
|
||||
|
||||
// Prepare the chart data
|
||||
this._prepareChartData();
|
||||
});
|
||||
@@ -81,6 +120,14 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
private refreshComponent(){
|
||||
|
||||
let currentUrl = this.router.url;
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
this.router.onSameUrlNavigation = 'reload';
|
||||
this.router.navigate([currentUrl]);
|
||||
}
|
||||
|
||||
private _deviceDataTemperatureSeries() {
|
||||
var deviceTemperatureSeries = []
|
||||
|
||||
@@ -91,8 +138,11 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
if (!deviceSummary.temp_history){
|
||||
continue
|
||||
}
|
||||
|
||||
let deviceName = this.deviceTitle(deviceSummary.device)
|
||||
|
||||
var deviceSeriesMetadata = {
|
||||
name: `/dev/${deviceSummary.device.device_name}`,
|
||||
name: deviceName,
|
||||
data: []
|
||||
}
|
||||
|
||||
@@ -164,6 +214,26 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
deviceTitle(disk){
|
||||
|
||||
console.log(`Displaying Device ${disk.wwn} with: ${this.config.dashboardDisplay}`)
|
||||
let titleParts = []
|
||||
if (disk.host_id) titleParts.push(disk.host_id)
|
||||
|
||||
//add device identifier (fallback to generated device name)
|
||||
titleParts.push(deviceDisplayTitle(disk, this.config.dashboardDisplay) || deviceDisplayTitle(disk, 'name'))
|
||||
|
||||
return titleParts.join(' - ')
|
||||
}
|
||||
|
||||
deviceSummariesForHostGroup(hostGroupWWNs: string[]) {
|
||||
let deviceSummaries = []
|
||||
for(let wwn of hostGroupWWNs){
|
||||
deviceSummaries.push(this.data.data.summary[wwn])
|
||||
}
|
||||
return deviceSummaries
|
||||
}
|
||||
|
||||
openDialog() {
|
||||
const dialogRef = this.dialog.open(DashboardSettingsComponent);
|
||||
|
||||
@@ -172,48 +242,29 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
});
|
||||
}
|
||||
|
||||
deviceTitle(disk){
|
||||
let title = []
|
||||
/*
|
||||
|
||||
if (disk.host_id) title.push(disk.host_id)
|
||||
DURATION_KEY_WEEK = "week"
|
||||
DURATION_KEY_MONTH = "month"
|
||||
DURATION_KEY_YEAR = "year"
|
||||
DURATION_KEY_FOREVER = "forever"
|
||||
*/
|
||||
|
||||
title.push(`/dev/${disk.device_name}`)
|
||||
changeSummaryTempDuration(durationKey: string){
|
||||
this.tempDurationKey = durationKey
|
||||
|
||||
if (disk.device_type && disk.device_type != 'scsi' && disk.device_type != 'ata'){
|
||||
title.push(disk.device_type)
|
||||
}
|
||||
this._smartService.getSummaryTempData(durationKey)
|
||||
.subscribe((data) => {
|
||||
|
||||
title.push(disk.model_name)
|
||||
// given a list of device temp history, override the data in the "summary" object.
|
||||
for(const wwn in this.data.data.summary) {
|
||||
// console.log(`Updating ${wwn}, length: ${this.data.data.summary[wwn].temp_history.length}`)
|
||||
this.data.data.summary[wwn].temp_history = data.data.temp_history[wwn] || []
|
||||
}
|
||||
|
||||
return title.join(' - ')
|
||||
}
|
||||
|
||||
deviceStatusString(deviceStatus){
|
||||
if(deviceStatus == 0){
|
||||
return "passed"
|
||||
} else {
|
||||
return "failed"
|
||||
}
|
||||
}
|
||||
|
||||
classDeviceLastUpdatedOn(deviceSummary){
|
||||
if (deviceSummary.device.device_status !== 0) {
|
||||
return 'text-red' // if the device has failed, always highlight in red
|
||||
} else if(deviceSummary.device.device_status === 0 && deviceSummary.smart){
|
||||
if(moment().subtract(14, 'd').isBefore(deviceSummary.smart.collector_date)){
|
||||
// this device was updated in the last 2 weeks.
|
||||
return 'text-green'
|
||||
} else if(moment().subtract(1, 'm').isBefore(deviceSummary.smart.collector_date)){
|
||||
// this device was updated in the last month
|
||||
return 'text-yellow'
|
||||
} else{
|
||||
// last updated more than a month ago.
|
||||
return 'text-red'
|
||||
}
|
||||
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
// Prepare the chart series data
|
||||
this.tempChart.updateSeries(this._deviceDataTemperatureSeries())
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,6 +278,4 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
return item.id || index;
|
||||
}
|
||||
|
||||
readonly humanizeDuration = humanizeDuration;
|
||||
|
||||
}
|
||||
|
||||
@@ -13,12 +13,13 @@ import { MatTableModule } from '@angular/material/table';
|
||||
import { NgApexchartsModule } from 'ng-apexcharts';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip'
|
||||
import { DashboardSettingsModule } from "app/layout/common/dashboard-settings/dashboard-settings.module";
|
||||
import { DashboardDeviceModule } from "app/layout/common/dashboard-device/dashboard-device.module";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DashboardComponent
|
||||
],
|
||||
imports : [
|
||||
imports: [
|
||||
RouterModule.forChild(dashboardRoutes),
|
||||
MatButtonModule,
|
||||
MatDividerModule,
|
||||
@@ -30,7 +31,8 @@ import { DashboardSettingsModule } from "app/layout/common/dashboard-settings/da
|
||||
MatTableModule,
|
||||
NgApexchartsModule,
|
||||
SharedModule,
|
||||
DashboardSettingsModule
|
||||
DashboardSettingsModule,
|
||||
DashboardDeviceModule
|
||||
]
|
||||
})
|
||||
export class DashboardModule
|
||||
|
||||
@@ -31,6 +31,6 @@ export class DashboardResolver implements Resolve<any>
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any>
|
||||
{
|
||||
return this._dashboardService.getData();
|
||||
return this._dashboardService.getSummaryData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export class DashboardService
|
||||
/**
|
||||
* Get data
|
||||
*/
|
||||
getData(): Observable<any>
|
||||
getSummaryData(): Observable<any>
|
||||
{
|
||||
return this._httpClient.get(getBasePath() + '/api/summary').pipe(
|
||||
tap((response: any) => {
|
||||
@@ -52,4 +52,14 @@ export class DashboardService
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getSummaryTempData(durationKey: string): Observable<any>
|
||||
{
|
||||
let params = {}
|
||||
if(durationKey){
|
||||
params["duration_key"] = durationKey
|
||||
}
|
||||
|
||||
return this._httpClient.get(getBasePath() + '/api/summary/temp', {params: params});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,60 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {deviceDisplayTitle} from "app/layout/common/dashboard-device/dashboard-device.component";
|
||||
|
||||
@Pipe({
|
||||
name: 'deviceSort'
|
||||
})
|
||||
export class DeviceSortPipe implements PipeTransform {
|
||||
|
||||
numericalStatus(device): number {
|
||||
if(!device.smart_results[0]){
|
||||
return 0
|
||||
} else if (device.smart_results[0].smart_status == 'passed'){
|
||||
return 1
|
||||
} else {
|
||||
return -1
|
||||
statusCompareFn(a: any, b: any) {
|
||||
function deviceStatus(deviceSummary): number {
|
||||
if(!deviceSummary.smart){
|
||||
return 0
|
||||
} else if (deviceSummary.device.device_status == 0){
|
||||
return 1
|
||||
} else {
|
||||
return deviceSummary.device.device_status * -1 // will return range from -1, -2, -3
|
||||
}
|
||||
}
|
||||
|
||||
let left = deviceStatus(a)
|
||||
let right = deviceStatus(b)
|
||||
|
||||
return left - right;
|
||||
}
|
||||
|
||||
titleCompareFn(dashboardDisplay: string) {
|
||||
return function (a: any, b: any){
|
||||
let _dashboardDisplay = dashboardDisplay
|
||||
let left = deviceDisplayTitle(a.device, _dashboardDisplay) || deviceDisplayTitle(a.device, 'name')
|
||||
let right = deviceDisplayTitle(b.device, _dashboardDisplay) || deviceDisplayTitle(b.device, 'name')
|
||||
|
||||
if( left < right )
|
||||
return -1;
|
||||
|
||||
if( left > right )
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
transform(devices: Array<unknown>, ...args: unknown[]): Array<unknown> {
|
||||
transform(deviceSummaries: Array<unknown>, sortBy = 'status', dashboardDisplay = "name"): Array<unknown> {
|
||||
let compareFn = undefined
|
||||
switch (sortBy) {
|
||||
case 'status':
|
||||
compareFn = this.statusCompareFn
|
||||
break;
|
||||
case 'title':
|
||||
compareFn = this.titleCompareFn(dashboardDisplay)
|
||||
break;
|
||||
}
|
||||
|
||||
//failed, unknown/empty, passed
|
||||
devices.sort((a: any, b: any) => {
|
||||
deviceSummaries.sort(compareFn);
|
||||
|
||||
let left = this.numericalStatus(a)
|
||||
let right = this.numericalStatus(b)
|
||||
|
||||
return left - right;
|
||||
});
|
||||
|
||||
|
||||
return devices;
|
||||
return deviceSummaries;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user