]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts
import 15.2.4
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / shared / datatable / table / table.component.ts
1 import {
2 AfterContentChecked,
3 ChangeDetectionStrategy,
4 ChangeDetectorRef,
5 Component,
6 EventEmitter,
7 Input,
8 NgZone,
9 OnChanges,
10 OnDestroy,
11 OnInit,
12 Output,
13 PipeTransform,
14 TemplateRef,
15 ViewChild
16 } from '@angular/core';
17
18 import {
19 DatatableComponent,
20 getterForProp,
21 SortDirection,
22 SortPropDir,
23 TableColumnProp
24 } from '@swimlane/ngx-datatable';
25 import * as _ from 'lodash';
26 import { Observable, Subject, Subscription, timer as observableTimer } from 'rxjs';
27
28 import { Icons } from '../../../shared/enum/icons.enum';
29 import { CellTemplate } from '../../enum/cell-template.enum';
30 import { CdTableColumn } from '../../models/cd-table-column';
31 import { CdTableColumnFilter } from '../../models/cd-table-column-filter';
32 import { CdTableColumnFiltersChange } from '../../models/cd-table-column-filters-change';
33 import { CdTableFetchDataContext } from '../../models/cd-table-fetch-data-context';
34 import { CdTableSelection } from '../../models/cd-table-selection';
35 import { CdUserConfig } from '../../models/cd-user-config';
36
37 @Component({
38 selector: 'cd-table',
39 templateUrl: './table.component.html',
40 styleUrls: ['./table.component.scss'],
41 changeDetection: ChangeDetectionStrategy.OnPush
42 })
43 export class TableComponent implements AfterContentChecked, OnInit, OnChanges, OnDestroy {
44 @ViewChild(DatatableComponent, { static: true })
45 table: DatatableComponent;
46 @ViewChild('tableCellBoldTpl', { static: true })
47 tableCellBoldTpl: TemplateRef<any>;
48 @ViewChild('sparklineTpl', { static: true })
49 sparklineTpl: TemplateRef<any>;
50 @ViewChild('routerLinkTpl', { static: true })
51 routerLinkTpl: TemplateRef<any>;
52 @ViewChild('checkIconTpl', { static: true })
53 checkIconTpl: TemplateRef<any>;
54 @ViewChild('perSecondTpl', { static: true })
55 perSecondTpl: TemplateRef<any>;
56 @ViewChild('executingTpl', { static: true })
57 executingTpl: TemplateRef<any>;
58 @ViewChild('classAddingTpl', { static: true })
59 classAddingTpl: TemplateRef<any>;
60 @ViewChild('badgeTpl', { static: true })
61 badgeTpl: TemplateRef<any>;
62 @ViewChild('mapTpl', { static: true })
63 mapTpl: TemplateRef<any>;
64 @ViewChild('truncateTpl', { static: true })
65 truncateTpl: TemplateRef<any>;
66 @ViewChild('rowDetailsTpl', { static: true })
67 rowDetailsTpl: TemplateRef<any>;
68
69 // This is the array with the items to be shown.
70 @Input()
71 data: any[];
72 // Each item -> { prop: 'attribute name', name: 'display name' }
73 @Input()
74 columns: CdTableColumn[];
75 // Each item -> { prop: 'attribute name', dir: 'asc'||'desc'}
76 @Input()
77 sorts?: SortPropDir[];
78 // Method used for setting column widths.
79 @Input()
80 columnMode? = 'flex';
81 // Display only actions in header (make sure to disable toolHeader) and use ".only-table-actions"
82 @Input()
83 onlyActionHeader? = false;
84 // Display the tool header, including reload button, pagination and search fields?
85 @Input()
86 toolHeader? = true;
87 // Display search field inside tool header?
88 @Input()
89 searchField? = true;
90 // Display the table header?
91 @Input()
92 header? = true;
93 // Display the table footer?
94 @Input()
95 footer? = true;
96 // Page size to show. Set to 0 to show unlimited number of rows.
97 @Input()
98 limit? = 10;
99 // Has the row details?
100 @Input()
101 hasDetails = false;
102
103 /**
104 * Auto reload time in ms - per default every 5s
105 * You can set it to 0, undefined or false to disable the auto reload feature in order to
106 * trigger 'fetchData' if the reload button is clicked.
107 * You can set it to a negative number to, on top of disabling the auto reload,
108 * prevent triggering fetchData when initializing the table.
109 */
110 @Input()
111 autoReload: any = 5000;
112
113 // Which row property is unique for a row. If the identifier is not specified in any
114 // column, then the property name of the first column is used. Defaults to 'id'.
115 @Input()
116 identifier = 'id';
117 // If 'true', then the specified identifier is used anyway, although it is not specified
118 // in any column. Defaults to 'false'.
119 @Input()
120 forceIdentifier = false;
121 // Allows other components to specify which type of selection they want,
122 // e.g. 'single' or 'multi'.
123 @Input()
124 selectionType: string = undefined;
125 // By default selected item details will be updated on table refresh, if data has changed
126 @Input()
127 updateSelectionOnRefresh: 'always' | 'never' | 'onChange' = 'onChange';
128 // By default expanded item details will be updated on table refresh, if data has changed
129 @Input()
130 updateExpandedOnRefresh: 'always' | 'never' | 'onChange' = 'onChange';
131
132 @Input()
133 autoSave = true;
134
135 // Enable this in order to search through the JSON of any used object.
136 @Input()
137 searchableObjects = false;
138
139 // Only needed to set if the classAddingTpl is used
140 @Input()
141 customCss?: { [css: string]: number | string | ((any: any) => boolean) };
142
143 // Columns that aren't displayed but can be used as filters
144 @Input()
145 extraFilterableColumns: CdTableColumn[] = [];
146
147 /**
148 * Should be a function to update the input data if undefined nothing will be triggered
149 *
150 * Sometimes it's useful to only define fetchData once.
151 * Example:
152 * Usage of multiple tables with data which is updated by the same function
153 * What happens:
154 * The function is triggered through one table and all tables will update
155 */
156 @Output()
157 fetchData = new EventEmitter();
158
159 /**
160 * This should be defined if you need access to the selection object.
161 *
162 * Each time the table selection changes, this will be triggered and
163 * the new selection object will be sent.
164 *
165 * @memberof TableComponent
166 */
167 @Output()
168 updateSelection = new EventEmitter();
169
170 @Output()
171 setExpandedRow = new EventEmitter();
172
173 /**
174 * This should be defined if you need access to the applied column filters.
175 *
176 * Each time the column filters changes, this will be triggered and
177 * the column filters change event will be sent.
178 *
179 * @memberof TableComponent
180 */
181 @Output() columnFiltersChanged = new EventEmitter<CdTableColumnFiltersChange>();
182
183 /**
184 * Use this variable to access the selected row(s).
185 */
186 selection = new CdTableSelection();
187
188 /**
189 * Use this variable to access the expanded row
190 */
191 expanded: any = undefined;
192
193 tableColumns: CdTableColumn[];
194 icons = Icons;
195 cellTemplates: {
196 [key: string]: TemplateRef<any>;
197 } = {};
198 search = '';
199 rows: any[] = [];
200 loadingIndicator = true;
201 loadingError = false;
202 paginationClasses = {
203 pagerLeftArrow: Icons.leftArrowDouble,
204 pagerRightArrow: Icons.rightArrowDouble,
205 pagerPrevious: Icons.leftArrow,
206 pagerNext: Icons.rightArrow
207 };
208 userConfig: CdUserConfig = {};
209 tableName: string;
210 localStorage = window.localStorage;
211 private saveSubscriber: Subscription;
212 private reloadSubscriber: Subscription;
213 private updating = false;
214
215 // Internal variable to check if it is necessary to recalculate the
216 // table columns after the browser window has been resized.
217 private currentWidth: number;
218
219 columnFilters: CdTableColumnFilter[] = [];
220 selectedFilter: CdTableColumnFilter;
221 get columnFiltered(): boolean {
222 return _.some(this.columnFilters, (filter) => {
223 return filter.value !== undefined;
224 });
225 }
226
227 constructor(private ngZone: NgZone, private cdRef: ChangeDetectorRef) {}
228
229 static prepareSearch(search: string) {
230 search = search.toLowerCase().replace(/,/g, '');
231 if (search.match(/['"][^'"]+['"]/)) {
232 search = search.replace(/['"][^'"]+['"]/g, (match: string) => {
233 return match.replace(/(['"])([^'"]+)(['"])/g, '$2').replace(/ /g, '+');
234 });
235 }
236 return search.split(' ').filter((word) => word);
237 }
238
239 ngOnInit() {
240 // ngx-datatable triggers calculations each time mouse enters a row,
241 // this will prevent that.
242 this.table.element.addEventListener('mouseenter', (e) => e.stopPropagation(), true);
243 this._addTemplates();
244 if (!this.sorts) {
245 // Check whether the specified identifier exists.
246 const exists = _.findIndex(this.columns, ['prop', this.identifier]) !== -1;
247 // Auto-build the sorting configuration. If the specified identifier doesn't exist,
248 // then use the property of the first column.
249 this.sorts = this.createSortingDefinition(
250 exists ? this.identifier : this.columns[0].prop + ''
251 );
252 // If the specified identifier doesn't exist and it is not forced to use it anyway,
253 // then use the property of the first column.
254 if (!exists && !this.forceIdentifier) {
255 this.identifier = this.columns[0].prop + '';
256 }
257 }
258
259 this.initUserConfig();
260 this.columns.forEach((c) => {
261 if (c.cellTransformation) {
262 c.cellTemplate = this.cellTemplates[c.cellTransformation];
263 }
264 if (!c.flexGrow) {
265 c.flexGrow = c.prop + '' === this.identifier ? 1 : 2;
266 }
267 if (!c.resizeable) {
268 c.resizeable = false;
269 }
270 });
271
272 this.initExpandCollapseColumn(); // If rows have details, add a column to expand or collapse the rows
273 this.initCheckboxColumn();
274 this.filterHiddenColumns();
275 this.initColumnFilters();
276 this.updateColumnFilterOptions();
277 // Load the data table content every N ms or at least once.
278 // Force showing the loading indicator if there are subscribers to the fetchData
279 // event. This is necessary because it has been set to False in useData() when
280 // this method was triggered by ngOnChanges().
281 if (this.fetchData.observers.length > 0) {
282 this.loadingIndicator = true;
283 }
284 if (_.isInteger(this.autoReload) && this.autoReload > 0) {
285 this.ngZone.runOutsideAngular(() => {
286 this.reloadSubscriber = observableTimer(0, this.autoReload).subscribe(() => {
287 this.ngZone.run(() => {
288 return this.reloadData();
289 });
290 });
291 });
292 } else if (!this.autoReload) {
293 this.reloadData();
294 } else {
295 this.useData();
296 }
297 }
298
299 initUserConfig() {
300 if (this.autoSave) {
301 this.tableName = this._calculateUniqueTableName(this.columns);
302 this._loadUserConfig();
303 this._initUserConfigAutoSave();
304 }
305 if (!this.userConfig.limit) {
306 this.userConfig.limit = this.limit;
307 }
308 if (!this.userConfig.sorts) {
309 this.userConfig.sorts = this.sorts;
310 }
311 if (!this.userConfig.columns) {
312 this.updateUserColumns();
313 } else {
314 this.columns.forEach((c, i) => {
315 c.isHidden = this.userConfig.columns[i].isHidden;
316 });
317 }
318 }
319
320 _calculateUniqueTableName(columns: any[]) {
321 const stringToNumber = (s: string) => {
322 if (!_.isString(s)) {
323 return 0;
324 }
325 let result = 0;
326 for (let i = 0; i < s.length; i++) {
327 result += s.charCodeAt(i) * i;
328 }
329 return result;
330 };
331 return columns
332 .reduce(
333 (result, value, index) =>
334 (stringToNumber(value.prop) + stringToNumber(value.name)) * (index + 1) + result,
335 0
336 )
337 .toString();
338 }
339
340 _loadUserConfig() {
341 const loaded = this.localStorage.getItem(this.tableName);
342 if (loaded) {
343 this.userConfig = JSON.parse(loaded);
344 }
345 }
346
347 _initUserConfigAutoSave() {
348 const source: Observable<any> = Observable.create(this._initUserConfigProxy.bind(this));
349 this.saveSubscriber = source.subscribe(this._saveUserConfig.bind(this));
350 }
351
352 _initUserConfigProxy(observer: Subject<any>) {
353 this.userConfig = new Proxy(this.userConfig, {
354 set(config, prop: string, value) {
355 config[prop] = value;
356 observer.next(config);
357 return true;
358 }
359 });
360 }
361
362 _saveUserConfig(config: any) {
363 this.localStorage.setItem(this.tableName, JSON.stringify(config));
364 }
365
366 updateUserColumns() {
367 this.userConfig.columns = this.columns.map((c) => ({
368 prop: c.prop,
369 name: c.name,
370 isHidden: !!c.isHidden
371 }));
372 }
373
374 /**
375 * Add a column containing a checkbox if selectionType is 'multiClick'.
376 */
377 initCheckboxColumn() {
378 if (this.selectionType === 'multiClick') {
379 this.columns.unshift({
380 prop: undefined,
381 resizeable: false,
382 sortable: false,
383 draggable: false,
384 checkboxable: true,
385 canAutoResize: false,
386 cellClass: 'cd-datatable-checkbox',
387 width: 30
388 });
389 }
390 }
391
392 /**
393 * Add a column to expand and collapse the table row if it 'hasDetails'
394 */
395 initExpandCollapseColumn() {
396 if (this.hasDetails) {
397 this.columns.unshift({
398 prop: undefined,
399 resizeable: false,
400 sortable: false,
401 draggable: false,
402 isHidden: false,
403 canAutoResize: false,
404 cellClass: 'cd-datatable-expand-collapse',
405 width: 40,
406 cellTemplate: this.rowDetailsTpl
407 });
408 }
409 }
410
411 filterHiddenColumns() {
412 this.tableColumns = this.columns.filter((c) => !c.isHidden);
413 }
414
415 initColumnFilters() {
416 let filterableColumns = _.filter(this.columns, { filterable: true });
417 filterableColumns = [...filterableColumns, ...this.extraFilterableColumns];
418 this.columnFilters = filterableColumns.map((col: CdTableColumn) => {
419 return {
420 column: col,
421 options: [],
422 value: col.filterInitValue
423 ? this.createColumnFilterOption(col.filterInitValue, col.pipe)
424 : undefined
425 };
426 });
427 this.selectedFilter = _.first(this.columnFilters);
428 }
429
430 private createColumnFilterOption(
431 value: any,
432 pipe?: PipeTransform
433 ): { raw: string; formatted: string } {
434 return {
435 raw: _.toString(value),
436 formatted: pipe ? pipe.transform(value) : _.toString(value)
437 };
438 }
439
440 updateColumnFilterOptions() {
441 // update all possible values in a column
442 this.columnFilters.forEach((filter) => {
443 let values: any[] = [];
444
445 if (_.isUndefined(filter.column.filterOptions)) {
446 // only allow types that can be easily converted into string
447 const pre = _.filter(_.map(this.data, filter.column.prop), (v) => {
448 return (_.isString(v) && v !== '') || _.isBoolean(v) || _.isFinite(v) || _.isDate(v);
449 });
450 values = _.sortedUniq(pre.sort());
451 } else {
452 values = filter.column.filterOptions;
453 }
454
455 const options = values.map((v) => this.createColumnFilterOption(v, filter.column.pipe));
456
457 // In case a previous value is not available anymore
458 if (filter.value && _.isUndefined(_.find(options, { raw: filter.value.raw }))) {
459 filter.value = undefined;
460 }
461
462 filter.options = options;
463 });
464 }
465
466 onSelectFilter(filter: CdTableColumnFilter) {
467 this.selectedFilter = filter;
468 }
469
470 onChangeFilter(filter: CdTableColumnFilter, option?: { raw: string; formatted: string }) {
471 filter.value = _.isEqual(filter.value, option) ? undefined : option;
472 this.updateFilter();
473 }
474
475 doColumnFiltering() {
476 const appliedFilters: CdTableColumnFiltersChange['filters'] = [];
477 let data = [...this.data];
478 let dataOut: any[] = [];
479 this.columnFilters.forEach((filter) => {
480 if (filter.value === undefined) {
481 return;
482 }
483 appliedFilters.push({
484 name: filter.column.name,
485 prop: filter.column.prop,
486 value: filter.value
487 });
488 // Separate data to filtered and filtered-out parts.
489 const parts = _.partition(data, (row) => {
490 // Use getter from ngx-datatable to handle props like 'sys_api.size'
491 const valueGetter = getterForProp(filter.column.prop);
492 const value = valueGetter(row, filter.column.prop);
493 if (_.isUndefined(filter.column.filterPredicate)) {
494 // By default, test string equal
495 return `${value}` === filter.value.raw;
496 } else {
497 // Use custom function to filter
498 return filter.column.filterPredicate(row, filter.value.raw);
499 }
500 });
501 data = parts[0];
502 dataOut = [...dataOut, ...parts[1]];
503 });
504
505 this.columnFiltersChanged.emit({
506 filters: appliedFilters,
507 data: data,
508 dataOut: dataOut
509 });
510
511 // Remove the selection if previously-selected rows are filtered out.
512 _.forEach(this.selection.selected, (selectedItem) => {
513 if (_.find(data, { [this.identifier]: selectedItem[this.identifier] }) === undefined) {
514 this.selection = new CdTableSelection();
515 this.onSelect(this.selection);
516 }
517 });
518 return data;
519 }
520
521 ngOnDestroy() {
522 if (this.reloadSubscriber) {
523 this.reloadSubscriber.unsubscribe();
524 }
525 if (this.saveSubscriber) {
526 this.saveSubscriber.unsubscribe();
527 }
528 }
529
530 ngAfterContentChecked() {
531 // If the data table is not visible, e.g. another tab is active, and the
532 // browser window gets resized, the table and its columns won't get resized
533 // automatically if the tab gets visible again.
534 // https://github.com/swimlane/ngx-datatable/issues/193
535 // https://github.com/swimlane/ngx-datatable/issues/193#issuecomment-329144543
536 if (this.table && this.table.element.clientWidth !== this.currentWidth) {
537 this.currentWidth = this.table.element.clientWidth;
538 // Recalculate the sizes of the grid.
539 this.table.recalculate();
540 // Mark the datatable as changed, Angular's change-detection will
541 // do the rest for us => the grid will be redrawn.
542 // Note, the ChangeDetectorRef variable is private, so we need to
543 // use this workaround to access it and make TypeScript happy.
544 const cdRef = _.get(this.table, 'cd');
545 cdRef.markForCheck();
546 }
547 }
548
549 _addTemplates() {
550 this.cellTemplates.bold = this.tableCellBoldTpl;
551 this.cellTemplates.checkIcon = this.checkIconTpl;
552 this.cellTemplates.sparkline = this.sparklineTpl;
553 this.cellTemplates.routerLink = this.routerLinkTpl;
554 this.cellTemplates.perSecond = this.perSecondTpl;
555 this.cellTemplates.executing = this.executingTpl;
556 this.cellTemplates.classAdding = this.classAddingTpl;
557 this.cellTemplates.badge = this.badgeTpl;
558 this.cellTemplates.map = this.mapTpl;
559 this.cellTemplates.truncate = this.truncateTpl;
560 }
561
562 useCustomClass(value: any): string {
563 if (!this.customCss) {
564 throw new Error('Custom classes are not set!');
565 }
566 const classes = Object.keys(this.customCss);
567 const css = Object.values(this.customCss)
568 .map((v, i) => ((_.isFunction(v) && v(value)) || v === value) && classes[i])
569 .filter((x) => x)
570 .join(' ');
571 return _.isEmpty(css) ? undefined : css;
572 }
573
574 ngOnChanges() {
575 this.useData();
576 }
577
578 setLimit(e: any) {
579 const value = parseInt(e.target.value, 10);
580 if (value > 0) {
581 this.userConfig.limit = value;
582 }
583 }
584
585 reloadData() {
586 if (!this.updating) {
587 this.loadingError = false;
588 const context = new CdTableFetchDataContext(() => {
589 // Do we have to display the error panel?
590 this.loadingError = context.errorConfig.displayError;
591 // Force data table to show no data?
592 if (context.errorConfig.resetData) {
593 this.data = [];
594 }
595 // Stop the loading indicator and reset the data table
596 // to the correct state.
597 this.useData();
598 });
599 this.fetchData.emit(context);
600 this.updating = true;
601 }
602 }
603
604 refreshBtn() {
605 this.loadingIndicator = true;
606 this.reloadData();
607 }
608
609 rowIdentity() {
610 return (row: any) => {
611 const id = row[this.identifier];
612 if (_.isUndefined(id)) {
613 throw new Error(`Wrong identifier "${this.identifier}" -> "${id}"`);
614 }
615 return id;
616 };
617 }
618
619 useData() {
620 if (!this.data) {
621 return; // Wait for data
622 }
623 this.updateColumnFilterOptions();
624 this.updateFilter();
625 this.reset();
626 this.updateSelected();
627 this.updateExpanded();
628 }
629
630 /**
631 * Reset the data table to correct state. This includes:
632 * - Disable loading indicator
633 * - Reset 'Updating' flag
634 */
635 reset() {
636 this.loadingIndicator = false;
637 this.updating = false;
638 }
639
640 /**
641 * After updating the data, we have to update the selected items
642 * because details may have changed,
643 * or some selected items may have been removed.
644 */
645 updateSelected() {
646 if (this.updateSelectionOnRefresh === 'never') {
647 return;
648 }
649 const newSelected: any[] = [];
650 this.selection.selected.forEach((selectedItem) => {
651 for (const row of this.data) {
652 if (selectedItem[this.identifier] === row[this.identifier]) {
653 newSelected.push(row);
654 }
655 }
656 });
657 if (
658 this.updateSelectionOnRefresh === 'onChange' &&
659 _.isEqual(this.selection.selected, newSelected)
660 ) {
661 return;
662 }
663 this.selection.selected = newSelected;
664 this.onSelect(this.selection);
665 }
666
667 updateExpanded() {
668 if (_.isUndefined(this.expanded) || this.updateExpandedOnRefresh === 'never') {
669 return;
670 }
671
672 const expandedId = this.expanded[this.identifier];
673 const newExpanded = _.find(this.data, (row) => expandedId === row[this.identifier]);
674
675 if (this.updateExpandedOnRefresh === 'onChange' && _.isEqual(this.expanded, newExpanded)) {
676 return;
677 }
678
679 this.expanded = newExpanded;
680 this.setExpandedRow.emit(newExpanded);
681 }
682
683 onSelect($event: any) {
684 this.selection.selected = $event['selected'];
685 this.updateSelection.emit(_.clone(this.selection));
686 }
687
688 toggleColumn($event: any) {
689 const prop: TableColumnProp = $event.target.name;
690 const hide = !$event.target.checked;
691 if (hide && this.tableColumns.length === 1) {
692 $event.target.checked = true;
693 return;
694 }
695 _.find(this.columns, (c: CdTableColumn) => c.prop === prop).isHidden = hide;
696 this.updateColumns();
697 }
698
699 updateColumns() {
700 this.updateUserColumns();
701 this.filterHiddenColumns();
702 const sortProp = this.userConfig.sorts[0].prop;
703 if (!_.find(this.tableColumns, (c: CdTableColumn) => c.prop === sortProp)) {
704 this.userConfig.sorts = this.createSortingDefinition(this.tableColumns[0].prop);
705 }
706 this.table.recalculate();
707 this.cdRef.detectChanges();
708 }
709
710 createSortingDefinition(prop: TableColumnProp): SortPropDir[] {
711 return [
712 {
713 prop: prop,
714 dir: SortDirection.asc
715 }
716 ];
717 }
718
719 changeSorting({ sorts }: any) {
720 this.userConfig.sorts = sorts;
721 }
722
723 onClearSearch() {
724 this.search = '';
725 this.updateFilter();
726 }
727
728 onClearFilters() {
729 this.columnFilters.forEach((filter) => {
730 filter.value = undefined;
731 });
732 this.selectedFilter = _.first(this.columnFilters);
733 this.updateFilter();
734 }
735
736 updateFilter() {
737 let rows = this.columnFilters.length !== 0 ? this.doColumnFiltering() : this.data;
738
739 if (this.search.length > 0 && rows) {
740 const columns = this.columns.filter((c) => c.cellTransformation !== CellTemplate.sparkline);
741 // update the rows
742 rows = this.subSearch(rows, TableComponent.prepareSearch(this.search), columns);
743 // Whenever the filter changes, always go back to the first page
744 this.table.offset = 0;
745 }
746
747 this.rows = rows;
748 }
749
750 subSearch(data: any[], currentSearch: string[], columns: CdTableColumn[]): any[] {
751 if (currentSearch.length === 0 || data.length === 0) {
752 return data;
753 }
754 const searchTerms: string[] = currentSearch.pop().replace(/\+/g, ' ').split(':');
755 const columnsClone = [...columns];
756 if (searchTerms.length === 2) {
757 columns = columnsClone.filter((c) => c.name.toLowerCase().indexOf(searchTerms[0]) !== -1);
758 }
759 data = this.basicDataSearch(_.last(searchTerms), data, columns);
760 // Checks if user searches for column but he is still typing
761 return this.subSearch(data, currentSearch, columnsClone);
762 }
763
764 basicDataSearch(searchTerm: string, rows: any[], columns: CdTableColumn[]) {
765 if (searchTerm.length === 0) {
766 return rows;
767 }
768 return rows.filter((row) => {
769 return (
770 columns.filter((col) => {
771 let cellValue: any = _.get(row, col.prop);
772
773 if (!_.isUndefined(col.pipe)) {
774 cellValue = col.pipe.transform(cellValue);
775 }
776 if (_.isUndefined(cellValue) || _.isNull(cellValue)) {
777 return false;
778 }
779
780 if (_.isArray(cellValue)) {
781 cellValue = cellValue.join(' ');
782 } else if (_.isNumber(cellValue) || _.isBoolean(cellValue)) {
783 cellValue = cellValue.toString();
784 }
785
786 if (_.isObjectLike(cellValue)) {
787 if (this.searchableObjects) {
788 cellValue = JSON.stringify(cellValue);
789 } else {
790 return false;
791 }
792 }
793
794 return cellValue.toLowerCase().indexOf(searchTerm) !== -1;
795 }).length > 0
796 );
797 });
798 }
799
800 getRowClass() {
801 // Return the function used to populate a row's CSS classes.
802 return () => {
803 return {
804 clickable: !_.isUndefined(this.selectionType)
805 };
806 };
807 }
808
809 toggleExpandRow(row: any, isExpanded: boolean) {
810 if (!isExpanded) {
811 // If current row isn't expanded, collapse others
812 this.expanded = row;
813 this.table.rowDetail.collapseAllRows();
814 this.setExpandedRow.emit(row);
815 } else {
816 // If all rows are closed, emit undefined
817 this.expanded = undefined;
818 this.setExpandedRow.emit(undefined);
819 }
820 this.table.rowDetail.toggleExpandRow(row);
821 }
822 }