adding device protocl and type to the. Adding class for parsing smartctl --scan json output, for device detection. added an example/test file for smartctl -x -j added a placeholder settings panel. moved dashboard & details compoonent out of "Admin" directory.

This commit is contained in:
Jason Kulatunga
2020-09-16 08:09:50 -07:00
parent 98415e625d
commit 5101a37964
27 changed files with 1957 additions and 25 deletions
@@ -0,0 +1,328 @@
<div class="flex flex-col flex-auto w-full p-8 xs:p-2">
<div class="flex flex-wrap w-full">
<div class="flex items-center justify-between w-full my-4 px-4 xs:pr-0">
<div class="mr-6">
<h2 class="m-0">Drive Details</h2>
<div class="text-secondary tracking-tight">Dive into S.M.A.R.T data</div>
</div>
<!-- Action buttons -->
<div class="flex items-center">
<button class="xs:hidden"
matTooltip="not yet implemented"
mat-stroked-button>
<mat-icon class="icon-size-20"
[svgIcon]="'save'"></mat-icon>
<span class="ml-2">Export</span>
</button>
<button class="ml-2 xs:hidden"
matTooltip="not yet implemented"
mat-stroked-button>
<mat-icon class="icon-size-20 rotate-90 mirror"
[svgIcon]="'tune'"></mat-icon>
<span class="ml-2">Settings</span>
</button>
<!-- Actions menu (visible on xs) -->
<div class="hidden xs:flex">
<button [matMenuTriggerFor]="actionsMenu"
mat-icon-button>
<mat-icon [svgIcon]="'more_vert'"></mat-icon>
</button>
<mat-menu #actionsMenu="matMenu">
<button mat-menu-item
matTooltip="not yet implemented">
<mat-icon class="icon-size-20"
[svgIcon]="'save'"></mat-icon>
<span class="ml-2">Export</span>
</button>
<button mat-menu-item
matTooltip="not yet implemented">
<mat-icon class="icon-size-20 rotate-90 mirror"
[svgIcon]="'tune'"></mat-icon>
<span class="ml-2">Settings</span>
</button>
</mat-menu>
</div>
</div>
</div>
<!-- Card -->
<div class="flex flex-auto w-1/4 p-4 lt-md:w-full">
<treo-card class="flex flex-auto p-4 pt-6 flex-col flex-auto filter-list">
<div class="flex items-center justify-between">
<div class="text-2xl font-semibold leading-tight">/dev/{{data.data.device_name}}</div>
</div>
<div class="flex flex-col my-2 grid grid-cols-2">
<div *ngIf="data.data.manufacturer" class="my-2 col-span-2 lt-md:col-span-1">
<div>{{data.data.manufacturer}}</div>
<div class="text-secondary text-md">Model Family</div>
</div>
<div class="my-2 col-span-2 lt-md:col-span-1">
<div>{{data.data.model_name}}</div>
<div class="text-secondary text-md">Device Model</div>
</div>
<div class="my-2 col-span-2 lt-md:col-span-1">
<div>{{data.data.serial_number}}</div>
<div class="text-secondary text-md">Serial Number</div>
</div>
<div class="my-2 col-span-2 lt-md:col-span-1">
<div>{{data.data.wwn}}</div>
<div class="text-secondary text-md">LU WWN Device Id</div>
</div>
<div class="my-2 col-span-2 lt-md:col-span-1">
<div>{{data.data.firmware}}</div>
<div class="text-secondary text-md">Firmware Version</div>
</div>
<div class="my-2 col-span-2 lt-md:col-span-1">
<div>{{data.data.capacity | fileSize}}</div>
<div class="text-secondary text-md">Capacity</div>
</div>
<div *ngIf="data.data.rotational_speed" class="my-2 col-span-2 lt-md:col-span-1">
<div>{{data.data.rotational_speed}} RPM</div>
<div class="text-secondary text-md">Rotation Rate</div>
</div>
<div *ngIf="data.data.device_protocol" class="my-2 col-span-2 lt-md:col-span-1">
<div>{{data.data.device_protocol}}</div>
<div class="text-secondary text-md">Protocol</div>
</div>
<div class="my-2 col-span-2 lt-md:col-span-1">
<div>{{data.data.smart_results[0]?.power_cycle_count}}</div>
<div class="text-secondary text-md">Power Cycle Count</div>
</div>
<div class="my-2 col-span-2 lt-md:col-span-1">
<div matTooltip="{{data.data.smart_results[0]?.power_on_hours}} hours">{{humanizeHours(data.data.smart_results[0]?.power_on_hours)}}</div>
<div class="text-secondary text-md">Powered On</div>
</div>
<div class="my-2 col-span-2 lt-md:col-span-1">
<div>{{data.data.smart_results[0]?.temp}}°C</div>
<div class="text-secondary text-md">Temperature</div>
</div>
</div>
</treo-card>
</div>
<!-- S.M.A.R.T. Data table -->
<div class="flex flex-auto w-3/4 p-4 lt-md:w-full">
<div class="flex flex-col flex-auto w-full bg-card shadow-md rounded ">
<div class="p-6">
<div class="font-bold text-md text-secondary uppercase tracking-wider">S.M.A.R.T {{data.data.device_protocol}} Attributes</div>
<div class="text-sm text-hint font-medium">{{this.smartAttributeDataSource.data.length}} visible, {{getHiddenAttributes()}} hidden</div>
</div>
<div class="overflow-auto">
<table class="w-full bg-transparent"
mat-table
matSort
[dataSource]="smartAttributeDataSource"
[trackBy]="trackByFn"
#smartAttributeTable>
<!-- Status -->
<ng-container matColumnDef="status">
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
mat-header-cell
mat-sort-header
*matHeaderCellDef>
<span class="whitespace-no-wrap">
Status
</span>
</th>
<td mat-cell
*matCellDef="let attribute">
<span class="inline-flex items-center font-bold text-xs px-2 py-2px rounded-full tracking-wide uppercase"
[ngClass]="{'red-200': attribute.status === 'failed',
'green-200': attribute.status === 'passed',
'yellow-200': attribute.status === 'warn'
}">
<span class="w-2 h-2 rounded-full mr-2"
[ngClass]="{'bg-red': attribute.status === 'failed',
'bg-green': attribute.status === 'passed',
'bg-yellow': attribute.status === 'warn'}"></span>
<span class="pr-2px leading-relaxed whitespace-no-wrap" matTooltip="{{attribute.status_reason}}">{{attribute.status}}</span>
</span>
</td>
</ng-container>
<!-- ID -->
<ng-container matColumnDef="id">
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
mat-header-cell
mat-sort-header
*matHeaderCellDef>
<span class="whitespace-no-wrap">
ID
</span>
</th>
<td mat-cell
*matCellDef="let attribute">
<span class="pr-6 font-medium text-sm text-secondary whitespace-no-wrap">
{{attribute.attribute_id}} ({{toHex(attribute.attribute_id)}})
</span>
</td>
</ng-container>
<!-- Name -->
<ng-container matColumnDef="name">
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
mat-header-cell
mat-sort-header
*matHeaderCellDef>
<span class="whitespace-no-wrap">
Name
</span>
</th>
<td mat-cell
*matCellDef="let attribute">
<span class="pr-6 whitespace-no-wrap" matTooltip="{{getAttributeDescription(attribute)}}">
{{attribute.name}} <mat-icon *ngIf="getAttributeDescription(attribute)" class="icon-size-10" [svgIcon]="'info'"></mat-icon>
</span>
</td>
</ng-container>
<!-- Value -->
<ng-container matColumnDef="value">
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
mat-header-cell
mat-sort-header
*matHeaderCellDef>
<span class="whitespace-no-wrap">
Value
</span>
</th>
<td mat-cell
*matCellDef="let attribute">
<span class="pr-6 whitespace-no-wrap" matTooltip="{{getAttributeValueType(attribute)}}">
{{getAttributeValue(attribute)}}
</span>
</td>
</ng-container>
<!-- Worst -->
<ng-container matColumnDef="worst">
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
mat-header-cell
mat-sort-header
*matHeaderCellDef>
<span class="whitespace-no-wrap">
Worst
</span>
</th>
<td mat-cell
*matCellDef="let attribute">
<span class="pr-6 whitespace-no-wrap">
{{getAttributeWorst(attribute)}}
</span>
</td>
</ng-container>
<!-- Threshold -->
<ng-container matColumnDef="thresh">
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
mat-header-cell
mat-sort-header
*matHeaderCellDef>
<span class="whitespace-no-wrap">
Threshold
</span>
</th>
<td mat-cell
*matCellDef="let attribute">
<span class="pr-6 whitespace-no-wrap">
{{getAttributeThreshold(attribute)}}
</span>
</td>
</ng-container>
<!-- Ideal -->
<ng-container matColumnDef="ideal">
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
mat-header-cell
mat-sort-header
*matHeaderCellDef>
<span class="whitespace-no-wrap">
Ideal
</span>
</th>
<td mat-cell
*matCellDef="let attribute">
<span class="pr-6 font-medium whitespace-no-wrap">
{{getAttributeIdeal(attribute) }}
</span>
</td>
</ng-container>
<!-- Observed Failure Rate -->
<ng-container matColumnDef="failure">
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
mat-header-cell
mat-sort-header
*matHeaderCellDef>
<span class="whitespace-no-wrap" matTooltip="Failure rate is based on data provided by BackBlaze. The current attribute value is matched against the observed failure categories and an annual failure rate is determined.">
Failure Rate <mat-icon [svgIcon]="'info'"></mat-icon>
</span>
</th>
<td mat-cell
*matCellDef="let attribute">
<span class="pr-6 font-medium whitespace-no-wrap">
{{attribute.failure_rate | percent}}
</span>
</td>
</ng-container>
<!-- History -->
<ng-container matColumnDef="history">
<th class="bg-cool-gray-50 dark:bg-cool-gray-700 border-t"
mat-header-cell
mat-sort-header
*matHeaderCellDef>
<span class="whitespace-no-wrap">
History
</span>
</th>
<td mat-cell
*matCellDef="let attribute">
<span class="font-medium whitespace-no-wrap">
<apx-chart
[series]="attribute.chartData"
[chart]="commonSparklineOptions.chart"
[tooltip]="commonSparklineOptions.tooltip"
[stroke]="commonSparklineOptions.stroke"
[annotations]="attribute.chartDataReferenceLine"
></apx-chart>
</span>
</td>
</ng-container>
<!-- Footer -->
<ng-container matColumnDef="recentOrdersTableFooter">
<td class="px-3 border-none"
mat-footer-cell
*matFooterCellDef
colspan="6">
<button mat-button
(click)="toggleOnlyCritical()"
[color]="'primary'">
<span *ngIf="onlyCritical">Show all attributes</span>
<span *ngIf="!onlyCritical">Show critical attributes</span>
</button>
</td>
</ng-container>
<tr mat-header-row
*matHeaderRowDef="smartAttributeTableColumns"></tr>
<tr class="attribute-row h-16"
mat-row
[ngClass]="{'yellow-50': getAttributeCritical(row)}"
*matRowDef="let row; columns: smartAttributeTableColumns;"></tr>
<tr class="h-16"
mat-footer-row
*matFooterRowDef="['recentOrdersTableFooter']"></tr>
</table>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,24 @@
@import 'treo';
detail {
}
// -----------------------------------------------------------------------------------------------------
// @ Theming
// -----------------------------------------------------------------------------------------------------
@include treo-theme {
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$is-dark: map-get($theme, is-dark);
detail {
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DetailComponent } from './detail.component';
describe('DetailComponent', () => {
let component: DetailComponent;
let fixture: ComponentFixture<DetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DetailComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,311 @@
import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {ApexOptions} from "ng-apexcharts";
import {MatTableDataSource} from "@angular/material/table";
import {MatSort} from "@angular/material/sort";
import {Subject} from "rxjs";
import {DetailService} from "./detail.service";
import {takeUntil} from "rxjs/operators";
import {fadeOut} from "../../../@treo/animations/fade";
@Component({
selector: 'detail',
templateUrl: './detail.component.html',
styleUrls: ['./detail.component.scss']
})
export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
onlyCritical: boolean = true;
data: any;
commonSparklineOptions: Partial<ApexOptions>;
smartAttributeDataSource: MatTableDataSource<any>;
smartAttributeTableColumns: string[];
@ViewChild('smartAttributeTable', {read: MatSort})
smartAttributeTableMatSort: MatSort;
// Private
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {DetailService} _detailService
*/
constructor(
private _detailService: DetailService
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
// Set the defaults
this.smartAttributeDataSource = new MatTableDataSource();
// this.recentTransactionsTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh'];
this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh','ideal', 'failure', 'history'];
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the data
this._detailService.data$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((data) => {
// Store the data
this.data = data;
// Store the table data
this.smartAttributeDataSource.data = this._generateSmartAttributeTableDataSource(data.data.smart_results);
// Prepare the chart data
this._prepareChartData();
});
}
/**
* After view init
*/
ngAfterViewInit(): void
{
// Make the data source sortable
this.smartAttributeDataSource.sort = this.smartAttributeTableMatSort;
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
getAttributeDescription(attribute_data){
return this.data.metadata[attribute_data.attribute_id]?.description
}
getAttributeValue(attribute_data){
if(this.isAta()) {
let attribute_metadata = this.data.metadata[attribute_data.attribute_id]
if (attribute_metadata.display_type == "raw") {
return attribute_data.raw_value
} else if (attribute_metadata.display_type == "transformed" && attribute_data.transformed_value) {
return attribute_data.transformed_value
} else {
return attribute_data.value
}
}
else{
return attribute_data.value
}
}
getAttributeValueType(attribute_data){
if(this.isAta()) {
let attribute_metadata = this.data.metadata[attribute_data.attribute_id]
return attribute_metadata.display_type
} else {
return ''
}
}
getAttributeIdeal(attribute_data){
if(this.isAta()){
return this.data.metadata[attribute_data.attribute_id]?.display_type == "raw" ? this.data.metadata[attribute_data.attribute_id]?.ideal : ''
} else {
return this.data.metadata[attribute_data.attribute_id]?.ideal
}
}
getAttributeWorst(attribute_data){
return this.data.metadata[attribute_data.attribute_id]?.display_type == "normalized" ? attribute_data.worst : ''
}
getAttributeThreshold(attribute_data){
if(this.isAta()){
if (this.data.metadata[attribute_data.attribute_id]?.display_type == "normalized"){
return attribute_data.thresh
} else {
// if(this.data.metadata[attribute_data.attribute_id].observed_thresholds){
//
// } else {
// }
// return ''
return attribute_data.thresh
}
} else {
return (attribute_data.thresh == -1 ? '' : attribute_data.thresh )
}
}
getAttributeCritical(attribute_data){
return this.data.metadata[attribute_data.attribute_id]?.critical
}
getHiddenAttributes(){
let attributes_list
if(this.isAta()){
attributes_list = this.data.data.smart_results[0]?.ata_attributes
} else if(this.isNvme()){
attributes_list = this.data.data.smart_results[0]?.nvme_attributes
} else {
attributes_list = this.data.data.smart_results[0]?.scsi_attributes
}
return attributes_list.length - this.smartAttributeDataSource.data.length
}
isAta(): boolean {
return this.data.data.device_protocol == 'ATA'
}
isScsi(): boolean {
return this.data.data.device_protocol == 'SCSI'
}
isNvme(): boolean {
return this.data.data.device_protocol == 'NVMe'
}
private _generateSmartAttributeTableDataSource(smart_results){
var smartAttributeDataSource = [];
if(smart_results.length == 0){
return smartAttributeDataSource
}
var latest_smart_result = smart_results[0];
let attributes_list = []
if(this.isScsi()) {
this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'history'];
attributes_list = latest_smart_result.scsi_attributes
} else if(this.isNvme()){
this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'ideal', 'history'];
attributes_list = latest_smart_result.nvme_attributes
} else {
//ATA
attributes_list = latest_smart_result.ata_attributes
this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'worst', 'thresh','ideal', 'failure', 'history'];
}
for(let attr of attributes_list){
//chart history data
if (!attr.chartData) {
var rawHistory = (attr.history || []).map(hist_attr => this.getAttributeValue(hist_attr)).reverse()
rawHistory.push(this.getAttributeValue(attr))
attr.chartData = [
{
name: "chart-line-sparkline",
data: rawHistory
}
]
}
//determine when to include the attributes in table.
if(!this.onlyCritical || this.onlyCritical && this.data.metadata[attr.attribute_id]?.critical || attr.value < attr.thresh){
smartAttributeDataSource.push(attr)
}
}
return smartAttributeDataSource
}
/**
* Prepare the chart data from the data
*
* @private
*/
private _prepareChartData(): void
{
// Account balance
this.commonSparklineOptions = {
chart: {
type: "bar",
width: 100,
height: 25,
sparkline: {
enabled: true
},
animations: {
enabled: false
}
},
tooltip: {
fixed: {
enabled: false
},
x: {
show: false
},
y: {
title: {
formatter: function(seriesName) {
return "";
}
}
},
marker: {
show: false
}
},
stroke: {
width: 2,
colors: ['#667EEA']
}
};
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
toHex(decimalNumb){
return "0x" + Number(decimalNumb).toString(16).padStart(2, '0').toUpperCase()
}
toggleOnlyCritical(){
this.onlyCritical = !this.onlyCritical
this.smartAttributeDataSource.data = this._generateSmartAttributeTableDataSource(this.data.data.smart_results);
}
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return index;
// return item.id || index;
}
humanizeHours(hours: number): string {
if(!hours){
return '--'
}
var value: number
let unit = ""
if(hours > (24*365)){ //more than a year
value = Math.round((hours/(24*365)) * 10)/10
unit = "years"
} else if (hours > 24){
value = Math.round((hours/24) *10 )/10
unit = "days"
} else{
value = hours
unit = "hours"
}
return `${value} ${unit}`
}
}
@@ -0,0 +1,39 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { SharedModule } from 'app/shared/shared.module';
import { DetailComponent } from 'app/modules/detail/detail.component';
import { detailRoutes } from 'app/modules/detail/detail.routing';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
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 { MatTooltipModule } from '@angular/material/tooltip'
import { NgApexchartsModule } from 'ng-apexcharts';
import { TreoCardModule } from '@treo/components/card';
@NgModule({
declarations: [
DetailComponent
],
imports : [
RouterModule.forChild(detailRoutes),
MatButtonModule,
MatDividerModule,
MatTooltipModule,
MatIconModule,
MatMenuModule,
MatProgressBarModule,
MatSortModule,
MatTableModule,
NgApexchartsModule,
TreoCardModule,
SharedModule,
]
})
export class DetailModule
{
}
@@ -0,0 +1,36 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { DetailService } from 'app/modules/detail/detail.service';
@Injectable({
providedIn: 'root'
})
export class DetailResolver implements Resolve<any>
{
/**
* Constructor
*
* @param {FinanceService} _detailService
*/
constructor(
private _detailService: DetailService
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Resolver
*
* @param route
* @param state
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any>
{
return this._detailService.getData(route.params.wwn);
}
}
@@ -0,0 +1,13 @@
import { Route } from '@angular/router';
import { DetailComponent } from 'app/modules/detail/detail.component';
import {DetailResolver} from "./detail.resolvers";
export const detailRoutes: Route[] = [
{
path : '',
component: DetailComponent,
resolve : {
sales: DetailResolver
}
}
];
@@ -0,0 +1,54 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class DetailService
{
// Observables
private _data: BehaviorSubject<any>;
/**
* Constructor
*
* @param {HttpClient} _httpClient
*/
constructor(
private _httpClient: HttpClient
)
{
// Set the private defaults
this._data = new BehaviorSubject(null);
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Getter for data
*/
get data$(): Observable<any>
{
return this._data.asObservable();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Get data
*/
getData(wwn): Observable<any>
{
return this._httpClient.get(`/api/device/${wwn}/details`).pipe(
tap((response: any) => {
this._data.next(response);
})
);
}
}