adding setting to allow users to customize between binary vs SI/Metric units in UI.
fixes #330
This commit is contained in:
@@ -14,4 +14,5 @@ type Setting struct {
|
|||||||
|
|
||||||
SettingValueNumeric int `json:"setting_value_numeric"`
|
SettingValueNumeric int `json:"setting_value_numeric"`
|
||||||
SettingValueString string `json:"setting_value_string"`
|
SettingValueString string `json:"setting_value_string"`
|
||||||
|
SettingValueBool bool `json:"setting_value_bool"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -319,6 +319,12 @@ func (sr *scrutinyRepository) Migrate(ctx context.Context) error {
|
|||||||
SettingDataType: "string",
|
SettingDataType: "string",
|
||||||
SettingValueString: "celsius",
|
SettingValueString: "celsius",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
SettingKeyName: "file_size_si_units",
|
||||||
|
SettingKeyDescription: "File size in SI units (true | false)",
|
||||||
|
SettingDataType: "bool",
|
||||||
|
SettingValueBool: false,
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
SettingKeyName: "metrics.notify_level",
|
SettingKeyName: "metrics.notify_level",
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ func (sr *scrutinyRepository) LoadSettings(ctx context.Context) (*models.Setting
|
|||||||
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueNumeric)
|
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueNumeric)
|
||||||
} else if settingsEntry.SettingDataType == "string" {
|
} else if settingsEntry.SettingDataType == "string" {
|
||||||
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueString)
|
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueString)
|
||||||
|
} else if settingsEntry.SettingDataType == "bool" {
|
||||||
|
sr.appConfig.SetDefault(configKey, settingsEntry.SettingValueBool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,11 +69,13 @@ func (sr *scrutinyRepository) SaveSettings(ctx context.Context, settings models.
|
|||||||
settingsEntries[ndx].SettingValueNumeric = sr.appConfig.GetInt(configKey)
|
settingsEntries[ndx].SettingValueNumeric = sr.appConfig.GetInt(configKey)
|
||||||
} else if settingsEntry.SettingDataType == "string" {
|
} else if settingsEntry.SettingDataType == "string" {
|
||||||
settingsEntries[ndx].SettingValueString = sr.appConfig.GetString(configKey)
|
settingsEntries[ndx].SettingValueString = sr.appConfig.GetString(configKey)
|
||||||
|
} else if settingsEntry.SettingDataType == "bool" {
|
||||||
|
settingsEntries[ndx].SettingValueBool = sr.appConfig.GetBool(configKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// store in database.
|
// store in database.
|
||||||
//TODO: this should be `sr.gormClient.Updates(&settingsEntries).Error`
|
//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
|
err := sr.gormClient.Model(&models.SettingEntry{}).Where([]uint{settingsEntry.ID}).Select("setting_value_numeric", "setting_value_string", "setting_value_bool").Updates(settingsEntries[ndx]).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type SettingEntry struct {
|
|||||||
|
|
||||||
SettingValueNumeric int `json:"setting_value_numeric"`
|
SettingValueNumeric int `json:"setting_value_numeric"`
|
||||||
SettingValueString string `json:"setting_value_string"`
|
SettingValueString string `json:"setting_value_string"`
|
||||||
|
SettingValueBool bool `json:"setting_value_bool"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s SettingEntry) TableName() string {
|
func (s SettingEntry) TableName() string {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Settings struct {
|
|||||||
DashboardDisplay string `json:"dashboard_display" mapstructure:"dashboard_display"`
|
DashboardDisplay string `json:"dashboard_display" mapstructure:"dashboard_display"`
|
||||||
DashboardSort string `json:"dashboard_sort" mapstructure:"dashboard_sort"`
|
DashboardSort string `json:"dashboard_sort" mapstructure:"dashboard_sort"`
|
||||||
TemperatureUnit string `json:"temperature_unit" mapstructure:"temperature_unit"`
|
TemperatureUnit string `json:"temperature_unit" mapstructure:"temperature_unit"`
|
||||||
|
FileSizeSIUnits bool `json:"file_size_si_units" mapstructure:"file_size_si_units"`
|
||||||
|
|
||||||
Metrics struct {
|
Metrics struct {
|
||||||
NotifyLevel int `json:"notify_level" mapstructure:"notify_level"`
|
NotifyLevel int `json:"notify_level" mapstructure:"notify_level"`
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export interface AppConfig {
|
|||||||
|
|
||||||
temperature_unit?: TemperatureUnit;
|
temperature_unit?: TemperatureUnit;
|
||||||
|
|
||||||
|
file_size_si_units?: boolean;
|
||||||
|
|
||||||
// Settings from Scrutiny API
|
// Settings from Scrutiny API
|
||||||
|
|
||||||
metrics?: {
|
metrics?: {
|
||||||
@@ -69,6 +71,8 @@ export const appConfig: AppConfig = {
|
|||||||
dashboard_sort: 'status',
|
dashboard_sort: 'status',
|
||||||
|
|
||||||
temperature_unit: 'celsius',
|
temperature_unit: 'celsius',
|
||||||
|
file_size_si_units: false,
|
||||||
|
|
||||||
metrics: {
|
metrics: {
|
||||||
notify_level: MetricsNotifyLevel.Fail,
|
notify_level: MetricsNotifyLevel.Fail,
|
||||||
status_filter_attributes: MetricsStatusFilterAttributes.All,
|
status_filter_attributes: MetricsStatusFilterAttributes.All,
|
||||||
|
|||||||
+2
-1
@@ -58,7 +58,8 @@
|
|||||||
</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">Capacity</div>
|
<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
|
||||||
|
class="mt-2 font-medium text-3xl leading-none">{{ deviceSummary.device.capacity | fileSize:config.file_size_si_units}}</div>
|
||||||
</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">Powered On</div>
|
<div class="font-semibold text-xs text-hint uppercase tracking-wider leading-none">Powered On</div>
|
||||||
|
|||||||
+9
-1
@@ -37,12 +37,20 @@
|
|||||||
|
|
||||||
<div 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-xs:pr-3 gt-md:pr-3">
|
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
|
||||||
<mat-label>Temperature Display Unit</mat-label>
|
<mat-label>Temperature</mat-label>
|
||||||
<mat-select [(ngModel)]="temperatureUnit">
|
<mat-select [(ngModel)]="temperatureUnit">
|
||||||
<mat-option value="celsius">Celsius</mat-option>
|
<mat-option value="celsius">Celsius</mat-option>
|
||||||
<mat-option value="fahrenheit">Fahrenheit</mat-option>
|
<mat-option value="fahrenheit">Fahrenheit</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field class="flex-auto gt-xs:pr-3 gt-md:pr-3">
|
||||||
|
<mat-label>File Size</mat-label>
|
||||||
|
<mat-select [(ngModel)]="fileSizeSIUnits">
|
||||||
|
<mat-option [value]=true>SI Units (GB)</mat-option>
|
||||||
|
<mat-option [value]=false>Binary Units (GiB)</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col mt-5 gt-md:flex-row">
|
<div class="flex flex-col mt-5 gt-md:flex-row">
|
||||||
|
|||||||
+3
@@ -22,6 +22,7 @@ export class DashboardSettingsComponent implements OnInit {
|
|||||||
dashboardDisplay: string;
|
dashboardDisplay: string;
|
||||||
dashboardSort: string;
|
dashboardSort: string;
|
||||||
temperatureUnit: string;
|
temperatureUnit: string;
|
||||||
|
fileSizeSIUnits: boolean;
|
||||||
theme: string;
|
theme: string;
|
||||||
statusThreshold: number;
|
statusThreshold: number;
|
||||||
statusFilterAttributes: number;
|
statusFilterAttributes: number;
|
||||||
@@ -46,6 +47,7 @@ export class DashboardSettingsComponent implements OnInit {
|
|||||||
this.dashboardDisplay = config.dashboard_display;
|
this.dashboardDisplay = config.dashboard_display;
|
||||||
this.dashboardSort = config.dashboard_sort;
|
this.dashboardSort = config.dashboard_sort;
|
||||||
this.temperatureUnit = config.temperature_unit;
|
this.temperatureUnit = config.temperature_unit;
|
||||||
|
this.fileSizeSIUnits = config.file_size_si_units;
|
||||||
this.theme = config.theme;
|
this.theme = config.theme;
|
||||||
|
|
||||||
this.statusFilterAttributes = config.metrics.status_filter_attributes;
|
this.statusFilterAttributes = config.metrics.status_filter_attributes;
|
||||||
@@ -60,6 +62,7 @@ export class DashboardSettingsComponent implements OnInit {
|
|||||||
dashboard_display: this.dashboardDisplay as DashboardDisplay,
|
dashboard_display: this.dashboardDisplay as DashboardDisplay,
|
||||||
dashboard_sort: this.dashboardSort as DashboardSort,
|
dashboard_sort: this.dashboardSort as DashboardSort,
|
||||||
temperature_unit: this.temperatureUnit as TemperatureUnit,
|
temperature_unit: this.temperatureUnit as TemperatureUnit,
|
||||||
|
file_size_si_units: this.fileSizeSIUnits,
|
||||||
theme: this.theme as Theme,
|
theme: this.theme as Theme,
|
||||||
metrics: {
|
metrics: {
|
||||||
status_filter_attributes: this.statusFilterAttributes as MetricsStatusFilterAttributes,
|
status_filter_attributes: this.statusFilterAttributes as MetricsStatusFilterAttributes,
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
<div class="text-secondary text-md">Firmware Version</div>
|
<div class="text-secondary text-md">Firmware Version</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>{{device?.capacity | fileSize}}</div>
|
<div>{{device?.capacity | fileSize:config.file_size_si_units}}</div>
|
||||||
<div class="text-secondary text-md">Capacity</div>
|
<div class="text-secondary text-md">Capacity</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="device?.rotational_speed" class="my-2 col-span-2 lt-md:col-span-1">
|
<div *ngIf="device?.rotational_speed" class="my-2 col-span-2 lt-md:col-span-1">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FileSizePipe } from './file-size.pipe';
|
import {FileSizePipe} from './file-size.pipe';
|
||||||
|
|
||||||
describe('FileSizePipe', () => {
|
describe('FileSizePipe', () => {
|
||||||
it('create an instance', () => {
|
it('create an instance', () => {
|
||||||
@@ -10,23 +10,61 @@ describe('FileSizePipe', () => {
|
|||||||
const testCases = [
|
const testCases = [
|
||||||
{
|
{
|
||||||
'bytes': 1500,
|
'bytes': 1500,
|
||||||
'precision': undefined,
|
'si': false,
|
||||||
'result': '1 KB'
|
'result': '1.5 KiB'
|
||||||
},{
|
},
|
||||||
'bytes': 2_100_000_000,
|
{
|
||||||
'precision': undefined,
|
|
||||||
'result': '2.0 GB',
|
|
||||||
},{
|
|
||||||
'bytes': 1500,
|
'bytes': 1500,
|
||||||
'precision': 2,
|
'si': true,
|
||||||
'result': '1.46 KB',
|
'result': '1.5 kB'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'bytes': 5000,
|
||||||
|
'si': false,
|
||||||
|
'result': '4.9 KiB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'bytes': 5000,
|
||||||
|
'si': true,
|
||||||
|
'result': '5.0 kB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'bytes': 999_949,
|
||||||
|
'si': false,
|
||||||
|
'result': '976.5 KiB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'bytes': 999_949,
|
||||||
|
'si': true,
|
||||||
|
'result': '999.9 kB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'bytes': 999_950,
|
||||||
|
'si': true,
|
||||||
|
'result': '1.0 MB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'bytes': 1_551_859_712,
|
||||||
|
'si': false,
|
||||||
|
'result': '1.4 GiB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'bytes': 2_100_000_000,
|
||||||
|
'si': false,
|
||||||
|
'result': '2.0 GiB',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'bytes': 2_100_000_000,
|
||||||
|
'si': true,
|
||||||
|
'result': '2.1 GB',
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
testCases.forEach((test, index) => {
|
testCases.forEach((test, index) => {
|
||||||
it(`should correctly format bytes ${test.bytes}. (testcase: ${index + 1})`, () => {
|
it(`should correctly format bytes ${test.bytes}. (testcase: ${index + 1})`, () => {
|
||||||
// test
|
// test
|
||||||
const pipe = new FileSizePipe();
|
const pipe = new FileSizePipe();
|
||||||
const formatted = pipe.transform(test.bytes, test.precision)
|
const formatted = pipe.transform(test.bytes, test.si)
|
||||||
expect(formatted).toEqual(test.result);
|
expect(formatted).toEqual(test.result);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,75 +1,27 @@
|
|||||||
/**
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
* @license
|
|
||||||
* Copyright (c) 2019 Jonathan Catmull.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
import { Pipe, PipeTransform } from '@angular/core';
|
|
||||||
|
|
||||||
type unit = 'bytes' | 'KB' | 'MB' | 'GB' | 'TB' | 'PB';
|
@Pipe({name: 'fileSize'})
|
||||||
type unitPrecisionMap = {
|
|
||||||
[u in unit]: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultPrecisionMap: unitPrecisionMap = {
|
|
||||||
bytes: 0,
|
|
||||||
KB: 0,
|
|
||||||
MB: 1,
|
|
||||||
GB: 1,
|
|
||||||
TB: 2,
|
|
||||||
PB: 2
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Convert bytes into largest possible unit.
|
|
||||||
* Takes an precision argument that can be a number or a map for each unit.
|
|
||||||
* Usage:
|
|
||||||
* bytes | fileSize:precision
|
|
||||||
* @example
|
|
||||||
* // returns 1 KB
|
|
||||||
* {{ 1500 | fileSize }}
|
|
||||||
* @example
|
|
||||||
* // returns 2.1 GB
|
|
||||||
* {{ 2100000000 | fileSize }}
|
|
||||||
* @example
|
|
||||||
* // returns 1.46 KB
|
|
||||||
* {{ 1500 | fileSize:2 }}
|
|
||||||
*/
|
|
||||||
@Pipe({ name: 'fileSize' })
|
|
||||||
export class FileSizePipe implements PipeTransform {
|
export class FileSizePipe implements PipeTransform {
|
||||||
private readonly units: unit[] = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
||||||
|
|
||||||
transform(bytes: number = 0, precision: number | unitPrecisionMap = defaultPrecisionMap): string {
|
transform(bytes: number = 0, si = false, dp = 1): string {
|
||||||
if (isNaN(parseFloat(String(bytes))) || !isFinite(bytes)) return '?';
|
const thresh = si ? 1000 : 1024;
|
||||||
|
|
||||||
let unitIndex = 0;
|
if (Math.abs(bytes) < thresh) {
|
||||||
|
return bytes + ' B';
|
||||||
while (bytes >= 1024) {
|
|
||||||
bytes /= 1024;
|
|
||||||
unitIndex++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const unit = this.units[unitIndex];
|
const units = si
|
||||||
|
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||||
|
let u = -1;
|
||||||
|
const r = 10 ** dp;
|
||||||
|
|
||||||
if (typeof precision === 'number') {
|
do {
|
||||||
return `${bytes.toFixed(+precision)} ${unit}`;
|
bytes /= thresh;
|
||||||
}
|
++u;
|
||||||
return `${bytes.toFixed(precision[unit])} ${unit}`;
|
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
|
||||||
|
|
||||||
|
|
||||||
|
return bytes.toFixed(dp) + ' ' + units[u];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user