]> git.proxmox.com Git - ceph.git/blobdiff - ceph/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts
import ceph quincy 17.2.1
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / shared / datatable / table / table.component.ts
index 1eb859e18f1617536257434990e3b7eca7bec6ec..96bf2336e92d354a80bb91ffa730b9fb5c40a8ba 100644 (file)
@@ -11,28 +11,30 @@ import {
   OnInit,
   Output,
   PipeTransform,
+  SimpleChanges,
   TemplateRef,
   ViewChild
 } from '@angular/core';
 
 import {
   DatatableComponent,
+  getterForProp,
   SortDirection,
   SortPropDir,
   TableColumnProp
 } from '@swimlane/ngx-datatable';
-import { getterForProp } from '@swimlane/ngx-datatable/release/utils';
-import * as _ from 'lodash';
+import _ from 'lodash';
 import { Observable, Subject, Subscription, timer as observableTimer } from 'rxjs';
 
-import { Icons } from '../../../shared/enum/icons.enum';
-import { CellTemplate } from '../../enum/cell-template.enum';
-import { CdTableColumn } from '../../models/cd-table-column';
-import { CdTableColumnFilter } from '../../models/cd-table-column-filter';
-import { CdTableColumnFiltersChange } from '../../models/cd-table-column-filters-change';
-import { CdTableFetchDataContext } from '../../models/cd-table-fetch-data-context';
-import { CdTableSelection } from '../../models/cd-table-selection';
-import { CdUserConfig } from '../../models/cd-user-config';
+import { TableStatus } from '~/app/shared/classes/table-status';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableColumnFilter } from '~/app/shared/models/cd-table-column-filter';
+import { CdTableColumnFiltersChange } from '~/app/shared/models/cd-table-column-filters-change';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { CdUserConfig } from '~/app/shared/models/cd-user-config';
 
 @Component({
   selector: 'cd-table',
@@ -63,6 +65,8 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   mapTpl: TemplateRef<any>;
   @ViewChild('truncateTpl', { static: true })
   truncateTpl: TemplateRef<any>;
+  @ViewChild('rowDetailsTpl', { static: true })
+  rowDetailsTpl: TemplateRef<any>;
 
   // This is the array with the items to be shown.
   @Input()
@@ -94,11 +98,16 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   // Page size to show. Set to 0 to show unlimited number of rows.
   @Input()
   limit? = 10;
+  // Has the row details?
+  @Input()
+  hasDetails = false;
 
   /**
    * Auto reload time in ms - per default every 5s
    * You can set it to 0, undefined or false to disable the auto reload feature in order to
    * trigger 'fetchData' if the reload button is clicked.
+   * You can set it to a negative number to, on top of disabling the auto reload,
+   * prevent triggering fetchData when initializing the table.
    */
   @Input()
   autoReload: any = 5000;
@@ -118,6 +127,9 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   // By default selected item details will be updated on table refresh, if data has changed
   @Input()
   updateSelectionOnRefresh: 'always' | 'never' | 'onChange' = 'onChange';
+  // By default expanded item details will be updated on table refresh, if data has changed
+  @Input()
+  updateExpandedOnRefresh: 'always' | 'never' | 'onChange' = 'onChange';
 
   @Input()
   autoSave = true;
@@ -134,6 +146,9 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   @Input()
   extraFilterableColumns: CdTableColumn[] = [];
 
+  @Input()
+  status = new TableStatus();
+
   /**
    * Should be a function to update the input data if undefined nothing will be triggered
    *
@@ -157,6 +172,9 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   @Output()
   updateSelection = new EventEmitter();
 
+  @Output()
+  setExpandedRow = new EventEmitter();
+
   /**
    * This should be defined if you need access to the applied column filters.
    *
@@ -172,6 +190,17 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
    */
   selection = new CdTableSelection();
 
+  /**
+   * Use this variable to access the expanded row
+   */
+  expanded: any = undefined;
+
+  /**
+   * To prevent making changes to the original columns list, that might change
+   * how the table is renderer a second time, we now clone that list into a
+   * local variable and only use the clone.
+   */
+  localColumns: CdTableColumn[];
   tableColumns: CdTableColumn[];
   icons = Icons;
   cellTemplates: {
@@ -180,7 +209,6 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   search = '';
   rows: any[] = [];
   loadingIndicator = true;
-  loadingError = false;
   paginationClasses = {
     pagerLeftArrow: Icons.leftArrowDouble,
     pagerRightArrow: Icons.rightArrowDouble,
@@ -219,26 +247,29 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   }
 
   ngOnInit() {
+    this.localColumns = _.clone(this.columns);
+
     // ngx-datatable triggers calculations each time mouse enters a row,
     // this will prevent that.
-    this.table.element.addEventListener('mouseenter', (e) => e.stopPropagation(), true);
+    this.table.element.addEventListener('mouseenter', (e) => e.stopPropagation());
     this._addTemplates();
     if (!this.sorts) {
       // Check whether the specified identifier exists.
-      const exists = _.findIndex(this.columns, ['prop', this.identifier]) !== -1;
+      const exists = _.findIndex(this.localColumns, ['prop', this.identifier]) !== -1;
       // Auto-build the sorting configuration. If the specified identifier doesn't exist,
       // then use the property of the first column.
       this.sorts = this.createSortingDefinition(
-        exists ? this.identifier : this.columns[0].prop + ''
+        exists ? this.identifier : this.localColumns[0].prop + ''
       );
       // If the specified identifier doesn't exist and it is not forced to use it anyway,
       // then use the property of the first column.
       if (!exists && !this.forceIdentifier) {
-        this.identifier = this.columns[0].prop + '';
+        this.identifier = this.localColumns[0].prop + '';
       }
     }
+
     this.initUserConfig();
-    this.columns.forEach((c) => {
+    this.localColumns.forEach((c) => {
       if (c.cellTransformation) {
         c.cellTemplate = this.cellTemplates[c.cellTransformation];
       }
@@ -250,10 +281,13 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
       }
     });
 
+    this.initExpandCollapseColumn(); // If rows have details, add a column to expand or collapse the rows
     this.initCheckboxColumn();
     this.filterHiddenColumns();
     this.initColumnFilters();
     this.updateColumnFilterOptions();
+    // Notify all subscribers to reset their current selection.
+    this.updateSelection.emit(new CdTableSelection());
     // Load the data table content every N ms or at least once.
     // Force showing the loading indicator if there are subscribers to the fetchData
     // event. This is necessary because it has been set to False in useData() when
@@ -269,14 +303,20 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
           });
         });
       });
-    } else {
+    } else if (!this.autoReload) {
       this.reloadData();
+    } else {
+      this.useData();
+    }
+
+    if (this.selectionType === 'single') {
+      this.table.selectCheck = this.singleSelectCheck.bind(this);
     }
   }
 
   initUserConfig() {
     if (this.autoSave) {
-      this.tableName = this._calculateUniqueTableName(this.columns);
+      this.tableName = this._calculateUniqueTableName(this.localColumns);
       this._loadUserConfig();
       this._initUserConfigAutoSave();
     }
@@ -289,8 +329,12 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
     if (!this.userConfig.columns) {
       this.updateUserColumns();
     } else {
-      this.columns.forEach((c, i) => {
-        c.isHidden = this.userConfig.columns[i].isHidden;
+      this.userConfig.columns.forEach((col) => {
+        for (let i = 0; i < this.localColumns.length; i++) {
+          if (this.localColumns[i].prop === col.prop) {
+            this.localColumns[i].isHidden = col.isHidden;
+          }
+        }
       });
     }
   }
@@ -323,7 +367,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   }
 
   _initUserConfigAutoSave() {
-    const source: Observable<any> = Observable.create(this._initUserConfigProxy.bind(this));
+    const source: Observable<any> = new Observable(this._initUserConfigProxy.bind(this));
     this.saveSubscriber = source.subscribe(this._saveUserConfig.bind(this));
   }
 
@@ -342,7 +386,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   }
 
   updateUserColumns() {
-    this.userConfig.columns = this.columns.map((c) => ({
+    this.userConfig.columns = this.localColumns.map((c) => ({
       prop: c.prop,
       name: c.name,
       isHidden: !!c.isHidden
@@ -354,7 +398,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
    */
   initCheckboxColumn() {
     if (this.selectionType === 'multiClick') {
-      this.columns.unshift({
+      this.localColumns.unshift({
         prop: undefined,
         resizeable: false,
         sortable: false,
@@ -367,12 +411,31 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
     }
   }
 
+  /**
+   * Add a column to expand and collapse the table row if it 'hasDetails'
+   */
+  initExpandCollapseColumn() {
+    if (this.hasDetails) {
+      this.localColumns.unshift({
+        prop: undefined,
+        resizeable: false,
+        sortable: false,
+        draggable: false,
+        isHidden: false,
+        canAutoResize: false,
+        cellClass: 'cd-datatable-expand-collapse',
+        width: 40,
+        cellTemplate: this.rowDetailsTpl
+      });
+    }
+  }
+
   filterHiddenColumns() {
-    this.tableColumns = this.columns.filter((c) => !c.isHidden);
+    this.tableColumns = this.localColumns.filter((c) => !c.isHidden);
   }
 
   initColumnFilters() {
-    let filterableColumns = _.filter(this.columns, { filterable: true });
+    let filterableColumns = _.filter(this.localColumns, { filterable: true });
     filterableColumns = [...filterableColumns, ...this.extraFilterableColumns];
     this.columnFilters = filterableColumns.map((col: CdTableColumn) => {
       return {
@@ -530,8 +593,10 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
     return _.isEmpty(css) ? undefined : css;
   }
 
-  ngOnChanges() {
-    this.useData();
+  ngOnChanges(changes: SimpleChanges) {
+    if (changes.data && changes.data.currentValue) {
+      this.useData();
+    }
   }
 
   setLimit(e: any) {
@@ -543,10 +608,12 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
 
   reloadData() {
     if (!this.updating) {
-      this.loadingError = false;
+      this.status = new TableStatus();
       const context = new CdTableFetchDataContext(() => {
         // Do we have to display the error panel?
-        this.loadingError = context.errorConfig.displayError;
+        if (!!context.errorConfig.displayError) {
+          this.status = new TableStatus('danger', $localize`Failed to load data.`);
+        }
         // Force data table to show no data?
         if (context.errorConfig.resetData) {
           this.data = [];
@@ -583,6 +650,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
     this.updateFilter();
     this.reset();
     this.updateSelected();
+    this.updateExpanded();
   }
 
   /**
@@ -604,37 +672,62 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
     if (this.updateSelectionOnRefresh === 'never') {
       return;
     }
-    const newSelected: any[] = [];
+    const newSelected = new Set();
     this.selection.selected.forEach((selectedItem) => {
       for (const row of this.data) {
         if (selectedItem[this.identifier] === row[this.identifier]) {
-          newSelected.push(row);
+          newSelected.add(row);
         }
       }
     });
+    const newSelectedArray = Array.from(newSelected.values());
     if (
       this.updateSelectionOnRefresh === 'onChange' &&
-      _.isEqual(this.selection.selected, newSelected)
+      _.isEqual(this.selection.selected, newSelectedArray)
     ) {
       return;
     }
-    this.selection.selected = newSelected;
+    this.selection.selected = newSelectedArray;
     this.onSelect(this.selection);
   }
 
+  updateExpanded() {
+    if (_.isUndefined(this.expanded) || this.updateExpandedOnRefresh === 'never') {
+      return;
+    }
+
+    const expandedId = this.expanded[this.identifier];
+    const newExpanded = _.find(this.data, (row) => expandedId === row[this.identifier]);
+
+    if (this.updateExpandedOnRefresh === 'onChange' && _.isEqual(this.expanded, newExpanded)) {
+      return;
+    }
+
+    this.expanded = newExpanded;
+    this.setExpandedRow.emit(newExpanded);
+  }
+
   onSelect($event: any) {
-    this.selection.selected = $event['selected'];
+    // Ensure we do not process DOM 'select' events.
+    // https://github.com/swimlane/ngx-datatable/issues/899
+    if (_.has($event, 'selected')) {
+      this.selection.selected = $event['selected'];
+    }
     this.updateSelection.emit(_.clone(this.selection));
   }
 
-  toggleColumn($event: any) {
-    const prop: TableColumnProp = $event.target.name;
-    const hide = !$event.target.checked;
+  private singleSelectCheck(row: any) {
+    return this.selection.selected.indexOf(row) === -1;
+  }
+
+  toggleColumn(column: CdTableColumn) {
+    const prop: TableColumnProp = column.prop;
+    const hide = !column.isHidden;
     if (hide && this.tableColumns.length === 1) {
-      $event.target.checked = true;
+      column.isHidden = true;
       return;
     }
-    _.find(this.columns, (c: CdTableColumn) => c.prop === prop).isHidden = hide;
+    _.find(this.localColumns, (c: CdTableColumn) => c.prop === prop).isHidden = hide;
     this.updateColumns();
   }
 
@@ -678,8 +771,10 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   updateFilter() {
     let rows = this.columnFilters.length !== 0 ? this.doColumnFiltering() : this.data;
 
-    if (this.search.length > 0) {
-      const columns = this.columns.filter((c) => c.cellTransformation !== CellTemplate.sparkline);
+    if (this.search.length > 0 && rows) {
+      const columns = this.localColumns.filter(
+        (c) => c.cellTransformation !== CellTemplate.sparkline
+      );
       // update the rows
       rows = this.subSearch(rows, TableComponent.prepareSearch(this.search), columns);
       // Whenever the filter changes, always go back to the first page
@@ -693,10 +788,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
     if (currentSearch.length === 0 || data.length === 0) {
       return data;
     }
-    const searchTerms: string[] = currentSearch
-      .pop()
-      .replace(/\+/g, ' ')
-      .split(':');
+    const searchTerms: string[] = currentSearch.pop().replace(/\+/g, ' ').split(':');
     const columnsClone = [...columns];
     if (searchTerms.length === 2) {
       columns = columnsClone.filter((c) => c.name.toLowerCase().indexOf(searchTerms[0]) !== -1);
@@ -750,4 +842,19 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
       };
     };
   }
+
+  toggleExpandRow(row: any, isExpanded: boolean, event: any) {
+    event.stopPropagation();
+    if (!isExpanded) {
+      // If current row isn't expanded, collapse others
+      this.expanded = row;
+      this.table.rowDetail.collapseAllRows();
+      this.setExpandedRow.emit(row);
+    } else {
+      // If all rows are closed, emit undefined
+      this.expanded = undefined;
+      this.setExpandedRow.emit(undefined);
+    }
+    this.table.rowDetail.toggleExpandRow(row);
+  }
 }