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