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