]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: display devices' health information within a tabset 37784/head
authorKiefer Chang <kiefer.chang@suse.com>
Thu, 17 Sep 2020 12:58:47 +0000 (20:58 +0800)
committerKiefer Chang <kiefer.chang@suse.com>
Sun, 25 Oct 2020 01:47:25 +0000 (09:47 +0800)
Wrap all devices' health information within a tabset
instead of displaying them from top to bottom.

Add more guard in the HTML template to prevent referencing undefined
variables.

Fixes: https://tracker.ceph.com/issues/47494
Fixes: https://tracker.ceph.com/issues/43177
Signed-off-by: Kiefer Chang <kiefer.chang@suse.com>
(cherry picked from commit ba3350c7e8c755d5c84c1a027a3a173191cb898d)

Conflicts:
      src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.html
      src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.ts
      - Use ngx-bootstrap tabset for tabs.

src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.ts

index 3bc29c518dcfcccd2da0878f26c52180e92be151..b67dfd9ab9dfdf01a862611e2cff3da656b10454 100644 (file)
@@ -7,60 +7,79 @@
                   i18n>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</cd-alert-panel>
 
   <ng-container *ngIf="!error && !incompatible">
-    <cd-alert-panel *ngIf="!(data | keyvalue).length"
+    <cd-alert-panel *ngIf="isEmpty(data)"
                     type="info"
                     i18n>No SMART data available.</cd-alert-panel>
 
-    <tabset *ngFor="let device of data | keyvalue">
-      <tab [heading]="device.value.device + ' (' + device.value.identifier + ')'">
-        <ng-container *ngIf="device.value.error; else noError">
-          <cd-alert-panel id="alert-error"
-                          type="warning">{{ device.value.userMessage }}</cd-alert-panel>
-        </ng-container>
-        <ng-template #noError>
-          <!-- HDD/NVMe self test -->
-          <ng-container *ngIf="device.value.info.smart_status.passed; else selfTestFailed">
-            <cd-alert-panel id="alert-self-test-passed"
-                            size="slim"
-                            type="info"
-                            i18n-title
-                            title="SMART overall-health self-assessment test result"
-                            i18n>passed</cd-alert-panel>
+    <ng-container *ngIf="!isEmpty(data)">
+      <tabset>
+        <tab *ngFor="let device of data | keyvalue"
+             [heading]="device.value.device + ' (' + device.value.identifier + ')'">
+          <ng-container *ngIf="device.value.error; else noError">
+            <cd-alert-panel id="alert-error"
+                            type="warning">{{ device.value.userMessage }}</cd-alert-panel>
           </ng-container>
-          <ng-template #selfTestFailed>
-            <cd-alert-panel id="alert-self-test-failed"
+
+          <ng-template #noError>
+            <cd-alert-panel *ngIf="isEmpty(device.value.info?.smart_status); else hasSmartStatus"
+                            id="alert-self-test-unknown"
                             size="slim"
                             type="warning"
                             i18n-title
                             title="SMART overall-health self-assessment test result"
-                            i18n>failed</cd-alert-panel>
+                            i18n>unknown</cd-alert-panel>
+            <ng-template #hasSmartStatus>
+              <!-- HDD/NVMe self test -->
+              <ng-container *ngIf="device.value.info.smart_status.passed; else selfTestFailed">
+                <cd-alert-panel id="alert-self-test-passed"
+                                size="slim"
+                                type="info"
+                                i18n-title
+                                title="SMART overall-health self-assessment test result"
+                                i18n>passed</cd-alert-panel>
+              </ng-container>
+              <ng-template #selfTestFailed>
+                <cd-alert-panel id="alert-self-test-failed"
+                                size="slim"
+                                type="warning"
+                                i18n-title
+                                title="SMART overall-health self-assessment test result"
+                                i18n>failed</cd-alert-panel>
+              </ng-template>
+            </ng-template>
           </ng-template>
 
-          <tabset>
+          <tabset #innerTabset
+                  *ngIf="!isEmpty(device.value.info) || !isEmpty(device.value.smart)">
             <tab i18n-heading
                  heading="Device Information">
-              <cd-table-key-value [renderObjects]="true"
+              <cd-table-key-value *ngIf="!isEmpty(device.value.info)"
+                                  [renderObjects]="true"
                                   [data]="device.value.info"></cd-table-key-value>
+              <cd-alert-panel *ngIf="isEmpty(device.value.info)"
+                              id="alert-device-info-unavailable"
+                              type="info"
+                              i18n>No device information available for this device.</cd-alert-panel>
             </tab>
-
             <tab i18n-heading
                  heading="SMART">
-              <cd-table *ngIf="device.value.smart.attributes"
+              <cd-table *ngIf="device.value.smart?.attributes"
                         [data]="device.value.smart.attributes.table"
                         updateSelectionOnRefresh="never"
                         [columns]="smartDataColumns"></cd-table>
-              <cd-table-key-value *ngIf="device.value.smart.nvmeData"
+              <cd-table-key-value *ngIf="device.value.smart?.nvmeData"
                                   [renderObjects]="true"
                                   [data]="device.value.smart.nvmeData"
                                   updateSelectionOnRefresh="never"></cd-table-key-value>
-              <cd-alert-panel *ngIf="!device.value.smart.attributes && !device.value.smart.nvmeData"
+              <cd-alert-panel *ngIf="!device.value.smart?.attributes && !device.value.smart?.nvmeData"
+                              id="alert-device-smart-data-unavailable"
                               type="info"
                               i18n>No SMART data available for this device.</cd-alert-panel>
             </tab>
           </tabset>
-        </ng-template>
-      </tab>
-    </tabset>
+        </tab>
+      </tabset>
+    </ng-container>
   </ng-container>
 </ng-container>
 <ng-template #isLoading>
index 395608b34321b1f2adb74781ecbfe2630ff55a37..a0b57203ba4866e87c8658f5d09c20091a629277 100644 (file)
@@ -79,6 +79,34 @@ describe('OsdSmartListComponent', () => {
     component.ngOnChanges(changes);
   };
 
+  /**
+   * Verify an alert panel and its attributes.
+   *
+   * @param selector The CSS selector for the alert panel.
+   * @param panelTitle The title should be displayed.
+   * @param panelType Alert level of panel. Can be in `warning` or `info`.
+   * @param panelSize Pass `slim` for slim alert panel.
+   */
+  const verifyAlertPanel = (
+    selector: string,
+    panelTitle: string,
+    panelType: 'warning' | 'info',
+    panelSize?: 'slim'
+  ) => {
+    const alertPanel = fixture.debugElement.query(By.css(selector));
+    expect(component.incompatible).toBe(false);
+    expect(component.loading).toBe(false);
+
+    expect(alertPanel.attributes.type).toBe(panelType);
+    if (panelSize === 'slim') {
+      expect(alertPanel.attributes.title).toBe(panelTitle);
+      expect(alertPanel.attributes.size).toBe(panelSize);
+    } else {
+      const panelText = alertPanel.query(By.css('.alert-panel-text'));
+      expect(panelText.nativeElement.textContent).toBe(panelTitle);
+    }
+  };
+
   configureTestBed({
     declarations: [SmartListComponent],
     imports: [BrowserAnimationsModule, TabsModule, SharedModule, HttpClientTestingModule],
@@ -144,22 +172,61 @@ describe('OsdSmartListComponent', () => {
   it('should display info panel for passed self test', () => {
     initializeComponentWithData('hdd_v1');
     fixture.detectChanges();
-    const alertPanel = fixture.debugElement.query(By.css('cd-alert-panel#alert-self-test-passed'));
-    expect(component.incompatible).toBe(false);
-    expect(component.loading).toBe(false);
-    expect(alertPanel.attributes.size).toBe('slim');
-    expect(alertPanel.attributes.title).toBe('SMART overall-health self-assessment test result');
-    expect(alertPanel.attributes.type).toBe('info');
+    verifyAlertPanel(
+      'cd-alert-panel#alert-self-test-passed',
+      'SMART overall-health self-assessment test result',
+      'info',
+      'slim'
+    );
   });
 
   it('should display warning panel for failed self test', () => {
     initializeComponentWithData('hdd_v1', { 'smart_status.passed': false });
     fixture.detectChanges();
-    const alertPanel = fixture.debugElement.query(By.css('cd-alert-panel#alert-self-test-failed'));
-    expect(component.incompatible).toBe(false);
-    expect(component.loading).toBe(false);
-    expect(alertPanel.attributes.size).toBe('slim');
-    expect(alertPanel.attributes.title).toBe('SMART overall-health self-assessment test result');
-    expect(alertPanel.attributes.type).toBe('warning');
+    verifyAlertPanel(
+      'cd-alert-panel#alert-self-test-failed',
+      'SMART overall-health self-assessment test result',
+      'warning',
+      'slim'
+    );
+  });
+
+  it('should display warning panel for unknown self test', () => {
+    initializeComponentWithData('hdd_v1', { smart_status: undefined });
+    fixture.detectChanges();
+    verifyAlertPanel(
+      'cd-alert-panel#alert-self-test-unknown',
+      'SMART overall-health self-assessment test result',
+      'warning',
+      'slim'
+    );
+  });
+
+  it('should display info panel for empty device info', () => {
+    initializeComponentWithData('hdd_v1');
+    const deviceId: string = _.keys(component.data)[0];
+    component.data[deviceId]['info'] = {};
+    fixture.detectChanges();
+    component.innerTabset.tabs[0].active = true;
+    fixture.detectChanges();
+    verifyAlertPanel(
+      'tab.active cd-alert-panel#alert-device-info-unavailable',
+      'No device information available for this device.',
+      'info'
+    );
+  });
+
+  it('should display info panel for empty SMART data', () => {
+    initializeComponentWithData('hdd_v1');
+    const deviceId: string = _.keys(component.data)[0];
+    component.data[deviceId]['smart'] = {};
+    fixture.detectChanges();
+    component.innerTabset.tabs[1].active = true;
+    fixture.detectChanges();
+    verifyAlertPanel(
+      'tab.active cd-alert-panel#alert-device-smart-data-unavailable',
+      'No SMART data available for this device.',
+      'info'
+    );
   });
 });
index 4bccee8c4d92263fd96714147bcd3cd259f41f38..232b7eef40c66c77221cca793200807991863c6a 100644 (file)
@@ -1,7 +1,8 @@
-import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
+import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
 
 import { I18n } from '@ngx-translate/i18n-polyfill';
 import * as _ from 'lodash';
+import { TabsetComponent } from 'ngx-bootstrap/tabs';
 
 import { HostService } from '../../../shared/api/host.service';
 import { OsdService } from '../../../shared/api/osd.service';
@@ -20,6 +21,9 @@ import {
   styleUrls: ['./smart-list.component.scss']
 })
 export class SmartListComponent implements OnInit, OnChanges {
+  @ViewChild('innerTabset', { static: false })
+  innerTabset: TabsetComponent;
+
   @Input()
   osdId: number = null;
   @Input()
@@ -144,6 +148,10 @@ smartmontools is required to successfully retrieve data.`,
     }
   }
 
+  isEmpty(value: any) {
+    return _.isEmpty(value);
+  }
+
   ngOnInit() {
     this.smartDataColumns = [
       { prop: 'id', name: this.i18n('ID') },