You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
462 lines
16 KiB
462 lines
16 KiB
import humanizeDuration from 'humanize-duration';
|
|
import {AfterViewInit, Component, Inject, LOCALE_ID, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
|
import {ApexOptions} from 'ng-apexcharts';
|
|
import {AppConfig} from 'app/core/config/app.config';
|
|
import {DetailService} from './detail.service';
|
|
import {DetailSettingsComponent} from 'app/layout/common/detail-settings/detail-settings.component';
|
|
import {MatDialog} from '@angular/material/dialog';
|
|
import {MatSort} from '@angular/material/sort';
|
|
import {MatTableDataSource} from '@angular/material/table';
|
|
import {Subject} from 'rxjs';
|
|
import {ScrutinyConfigService} from 'app/core/config/scrutiny-config.service';
|
|
import {animate, state, style, transition, trigger} from '@angular/animations';
|
|
import {formatDate} from '@angular/common';
|
|
import {takeUntil} from 'rxjs/operators';
|
|
import {DeviceModel} from 'app/core/models/device-model';
|
|
import {SmartModel} from 'app/core/models/measurements/smart-model';
|
|
import {SmartAttributeModel} from 'app/core/models/measurements/smart-attribute-model';
|
|
import {AttributeMetadataModel} from 'app/core/models/thresholds/attribute-metadata-model';
|
|
import {DeviceStatusPipe} from 'app/shared/device-status.pipe';
|
|
|
|
// from Constants.go - these must match
|
|
const AttributeStatusPassed = 0
|
|
const AttributeStatusFailedSmart = 1
|
|
const AttributeStatusWarningScrutiny = 2
|
|
const AttributeStatusFailedScrutiny = 4
|
|
|
|
|
|
@Component({
|
|
selector: 'detail',
|
|
templateUrl: './detail.component.html',
|
|
styleUrls: ['./detail.component.scss'],
|
|
animations: [
|
|
trigger('detailExpand', [
|
|
state('collapsed', style({height: '0px', minHeight: '0'})),
|
|
state('expanded', style({height: '*'})),
|
|
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
|
|
]),
|
|
],
|
|
})
|
|
|
|
export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param {DetailService} _detailService
|
|
* @param {MatDialog} dialog
|
|
* @param {ScrutinyConfigService} _configService
|
|
* @param {string} locale
|
|
*/
|
|
constructor(
|
|
private _detailService: DetailService,
|
|
public dialog: MatDialog,
|
|
private _configService: ScrutinyConfigService,
|
|
@Inject(LOCALE_ID) public locale: string
|
|
) {
|
|
// 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'];
|
|
|
|
this.systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
|
|
}
|
|
|
|
config: AppConfig;
|
|
|
|
onlyCritical = true;
|
|
// data: any;
|
|
expandedAttribute: SmartAttributeModel | null;
|
|
|
|
metadata: { [p: string]: AttributeMetadataModel } | { [p: number]: AttributeMetadataModel };
|
|
device: DeviceModel;
|
|
// tslint:disable-next-line:variable-name
|
|
smart_results: SmartModel[];
|
|
|
|
commonSparklineOptions: Partial<ApexOptions>;
|
|
smartAttributeDataSource: MatTableDataSource<SmartAttributeModel>;
|
|
smartAttributeTableColumns: string[];
|
|
|
|
@ViewChild('smartAttributeTable', {read: MatSort})
|
|
smartAttributeTableMatSort: MatSort;
|
|
|
|
// Private
|
|
private _unsubscribeAll: Subject<any>;
|
|
private systemPrefersDark: boolean;
|
|
|
|
readonly humanizeDuration = humanizeDuration;
|
|
|
|
deviceStatusForModelWithThreshold = DeviceStatusPipe.deviceStatusForModelWithThreshold
|
|
// -----------------------------------------------------------------------------------------------------
|
|
// @ Lifecycle hooks
|
|
// -----------------------------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* On init
|
|
*/
|
|
ngOnInit(): void {
|
|
// Subscribe to config changes
|
|
this._configService.config$
|
|
.pipe(takeUntil(this._unsubscribeAll))
|
|
.subscribe((config: AppConfig) => {
|
|
|
|
this.config = config;
|
|
});
|
|
|
|
// Get the data
|
|
this._detailService.data$
|
|
.pipe(takeUntil(this._unsubscribeAll))
|
|
.subscribe((respWrapper) => {
|
|
|
|
// Store the data
|
|
// this.data = data;
|
|
this.device = respWrapper.data.device;
|
|
this.smart_results = respWrapper.data.smart_results
|
|
this.metadata = respWrapper.metadata;
|
|
|
|
|
|
// Store the table data
|
|
this.smartAttributeDataSource.data = this._generateSmartAttributeTableDataSource(this.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
|
|
// -----------------------------------------------------------------------------------------------------
|
|
|
|
getAttributeStatusName(attributeStatus: number): string {
|
|
// tslint:disable:no-bitwise
|
|
|
|
if (attributeStatus === AttributeStatusPassed) {
|
|
return 'passed'
|
|
|
|
} else if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0 || (attributeStatus & AttributeStatusFailedSmart) !== 0) {
|
|
return 'failed'
|
|
} else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0) {
|
|
return 'warn'
|
|
}
|
|
return ''
|
|
// tslint:enable:no-bitwise
|
|
}
|
|
|
|
getAttributeScrutinyStatusName(attributeStatus: number): string {
|
|
// tslint:disable:no-bitwise
|
|
if ((attributeStatus & AttributeStatusFailedScrutiny) !== 0) {
|
|
return 'failed'
|
|
} else if ((attributeStatus & AttributeStatusWarningScrutiny) !== 0) {
|
|
return 'warn'
|
|
} else {
|
|
return 'passed'
|
|
}
|
|
// tslint:enable:no-bitwise
|
|
}
|
|
|
|
getAttributeSmartStatusName(attributeStatus: number): string {
|
|
// tslint:disable:no-bitwise
|
|
if ((attributeStatus & AttributeStatusFailedSmart) !== 0) {
|
|
return 'failed'
|
|
} else {
|
|
return 'passed'
|
|
}
|
|
// tslint:enable:no-bitwise
|
|
}
|
|
|
|
|
|
getAttributeName(attributeData: SmartAttributeModel): string {
|
|
const attributeMetadata = this.metadata[attributeData.attribute_id]
|
|
if (!attributeMetadata) {
|
|
return 'Unknown Attribute Name'
|
|
} else {
|
|
return attributeMetadata.display_name
|
|
}
|
|
}
|
|
|
|
getAttributeDescription(attributeData: SmartAttributeModel): string {
|
|
const attributeMetadata = this.metadata[attributeData.attribute_id]
|
|
if (!attributeMetadata) {
|
|
return 'Unknown'
|
|
} else {
|
|
return attributeMetadata.description
|
|
}
|
|
}
|
|
|
|
getAttributeValue(attributeData: SmartAttributeModel): number {
|
|
if (this.isAta()) {
|
|
const attributeMetadata = this.metadata[attributeData.attribute_id]
|
|
if (!attributeMetadata) {
|
|
return attributeData.value
|
|
} else if (attributeMetadata.display_type === 'raw') {
|
|
return attributeData.raw_value
|
|
} else if (attributeMetadata.display_type === 'transformed' && attributeData.transformed_value) {
|
|
return attributeData.transformed_value
|
|
} else {
|
|
return attributeData.value
|
|
}
|
|
} else {
|
|
return attributeData.value
|
|
}
|
|
}
|
|
|
|
getAttributeValueType(attributeData: SmartAttributeModel): string {
|
|
if (this.isAta()) {
|
|
const attributeMetadata = this.metadata[attributeData.attribute_id]
|
|
if (!attributeMetadata) {
|
|
return ''
|
|
} else {
|
|
return attributeMetadata.display_type
|
|
}
|
|
} else {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
getAttributeIdeal(attributeData: SmartAttributeModel): string {
|
|
if (this.isAta()) {
|
|
return this.metadata[attributeData.attribute_id]?.display_type === 'raw' ? this.metadata[attributeData.attribute_id]?.ideal : ''
|
|
} else {
|
|
return this.metadata[attributeData.attribute_id]?.ideal
|
|
}
|
|
}
|
|
|
|
getAttributeWorst(attributeData: SmartAttributeModel): number | string {
|
|
const attributeMetadata = this.metadata[attributeData.attribute_id]
|
|
if (!attributeMetadata) {
|
|
return attributeData.worst
|
|
} else {
|
|
return attributeMetadata?.display_type === 'normalized' ? attributeData.worst : ''
|
|
}
|
|
}
|
|
|
|
getAttributeThreshold(attributeData: SmartAttributeModel): number | string {
|
|
if (this.isAta()) {
|
|
const attributeMetadata = this.metadata[attributeData.attribute_id]
|
|
if (!attributeMetadata || attributeMetadata.display_type === 'normalized') {
|
|
return attributeData.thresh
|
|
} else {
|
|
// if(this.data.metadata[attribute_data.attribute_id].observed_thresholds){
|
|
//
|
|
// } else {
|
|
// }
|
|
// return ''
|
|
return attributeData.thresh
|
|
}
|
|
} else {
|
|
return (attributeData.thresh === -1 ? '' : attributeData.thresh)
|
|
}
|
|
}
|
|
|
|
getAttributeCritical(attributeData: SmartAttributeModel): boolean {
|
|
return this.metadata[attributeData.attribute_id]?.critical
|
|
}
|
|
|
|
getHiddenAttributes(): number {
|
|
if (!this.smart_results || this.smart_results.length === 0) {
|
|
return 0
|
|
}
|
|
|
|
let attributesLength = 0
|
|
const attributes = this.smart_results[0]?.attrs
|
|
if (attributes) {
|
|
attributesLength = Object.keys(attributes).length
|
|
}
|
|
|
|
return attributesLength - this.smartAttributeDataSource.data.length
|
|
}
|
|
|
|
isAta(): boolean {
|
|
return this.device.device_protocol === 'ATA'
|
|
}
|
|
|
|
isScsi(): boolean {
|
|
return this.device.device_protocol === 'SCSI'
|
|
}
|
|
|
|
isNvme(): boolean {
|
|
return this.device.device_protocol === 'NVMe'
|
|
}
|
|
|
|
private _generateSmartAttributeTableDataSource(smartResults: SmartModel[]): SmartAttributeModel[] {
|
|
const smartAttributeDataSource: SmartAttributeModel[] = [];
|
|
|
|
if (smartResults.length === 0) {
|
|
return smartAttributeDataSource
|
|
}
|
|
const latestSmartResult = smartResults[0];
|
|
let attributes: { [p: string]: SmartAttributeModel } = {}
|
|
if (this.isScsi()) {
|
|
this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'history'];
|
|
attributes = latestSmartResult.attrs
|
|
} else if (this.isNvme()) {
|
|
this.smartAttributeTableColumns = ['status', 'name', 'value', 'thresh', 'ideal', 'history'];
|
|
attributes = latestSmartResult.attrs
|
|
} else {
|
|
// ATA
|
|
attributes = latestSmartResult.attrs
|
|
this.smartAttributeTableColumns = ['status', 'id', 'name', 'value', 'thresh', 'ideal', 'failure', 'history'];
|
|
}
|
|
|
|
for (const attrId in attributes) {
|
|
const attr = attributes[attrId]
|
|
|
|
// chart history data
|
|
if (!attr.chartData) {
|
|
|
|
|
|
const attrHistory = []
|
|
for (const smartResult of smartResults) {
|
|
// attrHistory.push(this.getAttributeValue(smart_result.attrs[attrId]))
|
|
|
|
const chartDatapoint = {
|
|
x: formatDate(smartResult.date, 'MMMM dd, yyyy - HH:mm', this.locale),
|
|
y: this.getAttributeValue(smartResult.attrs[attrId])
|
|
}
|
|
const attributeStatusName = this.getAttributeStatusName(smartResult.attrs[attrId].status)
|
|
if (attributeStatusName === 'failed') {
|
|
chartDatapoint['strokeColor'] = '#F05252'
|
|
chartDatapoint['fillColor'] = '#F05252'
|
|
} else if (attributeStatusName === 'warn') {
|
|
chartDatapoint['strokeColor'] = '#C27803'
|
|
chartDatapoint['fillColor'] = '#C27803'
|
|
}
|
|
attrHistory.push(chartDatapoint)
|
|
}
|
|
|
|
// var rawHistory = (attr.history || []).map(hist_attr => this.getAttributeValue(hist_attr)).reverse()
|
|
// rawHistory.push(this.getAttributeValue(attr))
|
|
|
|
attributes[attrId].chartData = [
|
|
{
|
|
name: 'chart-line-sparkline',
|
|
// attrHistory needs to be reversed, so the newest data is on the right
|
|
// fixes #339
|
|
data: attrHistory.reverse()
|
|
}
|
|
]
|
|
}
|
|
// determine when to include the attributes in table.
|
|
|
|
if (!this.onlyCritical || this.onlyCritical && this.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
|
|
}
|
|
},
|
|
// theme:{
|
|
// // @ts-ignore
|
|
// // mode:
|
|
// mode: 'dark',
|
|
// },
|
|
tooltip: {
|
|
fixed: {
|
|
enabled: false
|
|
},
|
|
x: {
|
|
show: true
|
|
},
|
|
y: {
|
|
title: {
|
|
formatter: (seriesName) => {
|
|
return '';
|
|
}
|
|
}
|
|
},
|
|
marker: {
|
|
show: false
|
|
},
|
|
theme: this.determineTheme(this.config)
|
|
|
|
},
|
|
stroke: {
|
|
width: 2,
|
|
colors: ['#667EEA']
|
|
}
|
|
};
|
|
}
|
|
|
|
private determineTheme(config: AppConfig): string {
|
|
if (config.theme === 'system') {
|
|
return this.systemPrefersDark ? 'dark' : 'light'
|
|
} else {
|
|
return config.theme
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------------------------
|
|
// @ Public methods
|
|
// -----------------------------------------------------------------------------------------------------
|
|
|
|
toHex(decimalNumb: number | string): string {
|
|
return '0x' + Number(decimalNumb).toString(16).padStart(2, '0').toUpperCase()
|
|
}
|
|
|
|
toggleOnlyCritical(): void {
|
|
this.onlyCritical = !this.onlyCritical
|
|
this.smartAttributeDataSource.data = this._generateSmartAttributeTableDataSource(this.smart_results);
|
|
}
|
|
|
|
openDialog(): void {
|
|
const dialogRef = this.dialog.open(DetailSettingsComponent);
|
|
|
|
dialogRef.afterClosed().subscribe(result => {
|
|
console.log(`Dialog result: ${result}`);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Track by function for ngFor loops
|
|
*
|
|
* @param index
|
|
* @param item
|
|
*/
|
|
trackByFn(index: number, item: any): any {
|
|
return index;
|
|
// return item.id || index;
|
|
}
|
|
|
|
}
|