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