]> git.proxmox.com Git - ceph.git/blob - 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
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 if (this.selectionType === 'single') {
313 this.table.selectCheck = this.singleSelectCheck.bind(this);
314 }
315 }
316
317 initUserConfig() {
318 if (this.autoSave) {
319 this.tableName = this._calculateUniqueTableName(this.localColumns);
320 this._loadUserConfig();
321 this._initUserConfigAutoSave();
322 }
323 if (!this.userConfig.limit) {
324 this.userConfig.limit = this.limit;
325 }
326 if (!this.userConfig.sorts) {
327 this.userConfig.sorts = this.sorts;
328 }
329 if (!this.userConfig.columns) {
330 this.updateUserColumns();
331 } else {
332 this.userConfig.columns.forEach((col) => {
333 for (let i = 0; i < this.localColumns.length; i++) {
334 if (this.localColumns[i].prop === col.prop) {
335 this.localColumns[i].isHidden = col.isHidden;
336 }
337 }
338 });
339 }
340 }
341
342 _calculateUniqueTableName(columns: any[]) {
343 const stringToNumber = (s: string) => {
344 if (!_.isString(s)) {
345 return 0;
346 }
347 let result = 0;
348 for (let i = 0; i < s.length; i++) {
349 result += s.charCodeAt(i) * i;
350 }
351 return result;
352 };
353 return columns
354 .reduce(
355 (result, value, index) =>
356 (stringToNumber(value.prop) + stringToNumber(value.name)) * (index + 1) + result,
357 0
358 )
359 .toString();
360 }
361
362 _loadUserConfig() {
363 const loaded = this.localStorage.getItem(this.tableName);
364 if (loaded) {
365 this.userConfig = JSON.parse(loaded);
366 }
367 }
368
369 _initUserConfigAutoSave() {
370 const source: Observable<any> = new Observable(this._initUserConfigProxy.bind(this));
371 this.saveSubscriber = source.subscribe(this._saveUserConfig.bind(this));
372 }
373
374 _initUserConfigProxy(observer: Subject<any>) {
375 this.userConfig = new Proxy(this.userConfig, {
376 set(config, prop: string, value) {
377 config[prop] = value;
378 observer.next(config);
379 return true;
380 }
381 });
382 }
383
384 _saveUserConfig(config: any) {
385 this.localStorage.setItem(this.tableName, JSON.stringify(config));
386 }
387
388 updateUserColumns() {
389 this.userConfig.columns = this.localColumns.map((c) => ({
390 prop: c.prop,
391 name: c.name,
392 isHidden: !!c.isHidden
393 }));
394 }
395
396 /**
397 * Add a column containing a checkbox if selectionType is 'multiClick'.
398 */
399 initCheckboxColumn() {
400 if (this.selectionType === 'multiClick') {
401 this.localColumns.unshift({
402 prop: undefined,
403 resizeable: false,
404 sortable: false,
405 draggable: false,
406 checkboxable: true,
407 canAutoResize: false,
408 cellClass: 'cd-datatable-checkbox',
409 width: 30
410 });
411 }
412 }
413
414 /**
415 * Add a column to expand and collapse the table row if it 'hasDetails'
416 */
417 initExpandCollapseColumn() {
418 if (this.hasDetails) {
419 this.localColumns.unshift({
420 prop: undefined,
421 resizeable: false,
422 sortable: false,
423 draggable: false,
424 isHidden: false,
425 canAutoResize: false,
426 cellClass: 'cd-datatable-expand-collapse',
427 width: 40,
428 cellTemplate: this.rowDetailsTpl
429 });
430 }
431 }
432
433 filterHiddenColumns() {
434 this.tableColumns = this.localColumns.filter((c) => !c.isHidden);
435 }
436
437 initColumnFilters() {
438 let filterableColumns = _.filter(this.localColumns, { filterable: true });
439 filterableColumns = [...filterableColumns, ...this.extraFilterableColumns];
440 this.columnFilters = filterableColumns.map((col: CdTableColumn) => {
441 return {
442 column: col,
443 options: [],
444 value: col.filterInitValue
445 ? this.createColumnFilterOption(col.filterInitValue, col.pipe)
446 : undefined
447 };
448 });
449 this.selectedFilter = _.first(this.columnFilters);
450 }
451
452 private createColumnFilterOption(
453 value: any,
454 pipe?: PipeTransform
455 ): { raw: string; formatted: string } {
456 return {
457 raw: _.toString(value),
458 formatted: pipe ? pipe.transform(value) : _.toString(value)
459 };
460 }
461
462 updateColumnFilterOptions() {
463 // update all possible values in a column
464 this.columnFilters.forEach((filter) => {
465 let values: any[] = [];
466
467 if (_.isUndefined(filter.column.filterOptions)) {
468 // only allow types that can be easily converted into string
469 const pre = _.filter(_.map(this.data, filter.column.prop), (v) => {
470 return (_.isString(v) && v !== '') || _.isBoolean(v) || _.isFinite(v) || _.isDate(v);
471 });
472 values = _.sortedUniq(pre.sort());
473 } else {
474 values = filter.column.filterOptions;
475 }
476
477 const options = values.map((v) => this.createColumnFilterOption(v, filter.column.pipe));
478
479 // In case a previous value is not available anymore
480 if (filter.value && _.isUndefined(_.find(options, { raw: filter.value.raw }))) {
481 filter.value = undefined;
482 }
483
484 filter.options = options;
485 });
486 }
487
488 onSelectFilter(filter: CdTableColumnFilter) {
489 this.selectedFilter = filter;
490 }
491
492 onChangeFilter(filter: CdTableColumnFilter, option?: { raw: string; formatted: string }) {
493 filter.value = _.isEqual(filter.value, option) ? undefined : option;
494 this.updateFilter();
495 }
496
497 doColumnFiltering() {
498 const appliedFilters: CdTableColumnFiltersChange['filters'] = [];
499 let data = [...this.data];
500 let dataOut: any[] = [];
501 this.columnFilters.forEach((filter) => {
502 if (filter.value === undefined) {
503 return;
504 }
505 appliedFilters.push({
506 name: filter.column.name,
507 prop: filter.column.prop,
508 value: filter.value
509 });
510 // Separate data to filtered and filtered-out parts.
511 const parts = _.partition(data, (row) => {
512 // Use getter from ngx-datatable to handle props like 'sys_api.size'
513 const valueGetter = getterForProp(filter.column.prop);
514 const value = valueGetter(row, filter.column.prop);
515 if (_.isUndefined(filter.column.filterPredicate)) {
516 // By default, test string equal
517 return `${value}` === filter.value.raw;
518 } else {
519 // Use custom function to filter
520 return filter.column.filterPredicate(row, filter.value.raw);
521 }
522 });
523 data = parts[0];
524 dataOut = [...dataOut, ...parts[1]];
525 });
526
527 this.columnFiltersChanged.emit({
528 filters: appliedFilters,
529 data: data,
530 dataOut: dataOut
531 });
532
533 // Remove the selection if previously-selected rows are filtered out.
534 _.forEach(this.selection.selected, (selectedItem) => {
535 if (_.find(data, { [this.identifier]: selectedItem[this.identifier] }) === undefined) {
536 this.selection = new CdTableSelection();
537 this.onSelect(this.selection);
538 }
539 });
540 return data;
541 }
542
543 ngOnDestroy() {
544 if (this.reloadSubscriber) {
545 this.reloadSubscriber.unsubscribe();
546 }
547 if (this.saveSubscriber) {
548 this.saveSubscriber.unsubscribe();
549 }
550 }
551
552 ngAfterContentChecked() {
553 // If the data table is not visible, e.g. another tab is active, and the
554 // browser window gets resized, the table and its columns won't get resized
555 // automatically if the tab gets visible again.
556 // https://github.com/swimlane/ngx-datatable/issues/193
557 // https://github.com/swimlane/ngx-datatable/issues/193#issuecomment-329144543
558 if (this.table && this.table.element.clientWidth !== this.currentWidth) {
559 this.currentWidth = this.table.element.clientWidth;
560 // Recalculate the sizes of the grid.
561 this.table.recalculate();
562 // Mark the datatable as changed, Angular's change-detection will
563 // do the rest for us => the grid will be redrawn.
564 // Note, the ChangeDetectorRef variable is private, so we need to
565 // use this workaround to access it and make TypeScript happy.
566 const cdRef = _.get(this.table, 'cd');
567 cdRef.markForCheck();
568 }
569 }
570
571 _addTemplates() {
572 this.cellTemplates.bold = this.tableCellBoldTpl;
573 this.cellTemplates.checkIcon = this.checkIconTpl;
574 this.cellTemplates.sparkline = this.sparklineTpl;
575 this.cellTemplates.routerLink = this.routerLinkTpl;
576 this.cellTemplates.perSecond = this.perSecondTpl;
577 this.cellTemplates.executing = this.executingTpl;
578 this.cellTemplates.classAdding = this.classAddingTpl;
579 this.cellTemplates.badge = this.badgeTpl;
580 this.cellTemplates.map = this.mapTpl;
581 this.cellTemplates.truncate = this.truncateTpl;
582 }
583
584 useCustomClass(value: any): string {
585 if (!this.customCss) {
586 throw new Error('Custom classes are not set!');
587 }
588 const classes = Object.keys(this.customCss);
589 const css = Object.values(this.customCss)
590 .map((v, i) => ((_.isFunction(v) && v(value)) || v === value) && classes[i])
591 .filter((x) => x)
592 .join(' ');
593 return _.isEmpty(css) ? undefined : css;
594 }
595
596 ngOnChanges(changes: SimpleChanges) {
597 if (changes.data && changes.data.currentValue) {
598 this.useData();
599 }
600 }
601
602 setLimit(e: any) {
603 const value = parseInt(e.target.value, 10);
604 if (value > 0) {
605 this.userConfig.limit = value;
606 }
607 }
608
609 reloadData() {
610 if (!this.updating) {
611 this.status = new TableStatus();
612 const context = new CdTableFetchDataContext(() => {
613 // Do we have to display the error panel?
614 if (!!context.errorConfig.displayError) {
615 this.status = new TableStatus('danger', $localize`Failed to load data.`);
616 }
617 // Force data table to show no data?
618 if (context.errorConfig.resetData) {
619 this.data = [];
620 }
621 // Stop the loading indicator and reset the data table
622 // to the correct state.
623 this.useData();
624 });
625 this.fetchData.emit(context);
626 this.updating = true;
627 }
628 }
629
630 refreshBtn() {
631 this.loadingIndicator = true;
632 this.reloadData();
633 }
634
635 rowIdentity() {
636 return (row: any) => {
637 const id = row[this.identifier];
638 if (_.isUndefined(id)) {
639 throw new Error(`Wrong identifier "${this.identifier}" -> "${id}"`);
640 }
641 return id;
642 };
643 }
644
645 useData() {
646 if (!this.data) {
647 return; // Wait for data
648 }
649 this.updateColumnFilterOptions();
650 this.updateFilter();
651 this.reset();
652 this.updateSelected();
653 this.updateExpanded();
654 }
655
656 /**
657 * Reset the data table to correct state. This includes:
658 * - Disable loading indicator
659 * - Reset 'Updating' flag
660 */
661 reset() {
662 this.loadingIndicator = false;
663 this.updating = false;
664 }
665
666 /**
667 * After updating the data, we have to update the selected items
668 * because details may have changed,
669 * or some selected items may have been removed.
670 */
671 updateSelected() {
672 if (this.updateSelectionOnRefresh === 'never') {
673 return;
674 }
675 const newSelected = new Set();
676 this.selection.selected.forEach((selectedItem) => {
677 for (const row of this.data) {
678 if (selectedItem[this.identifier] === row[this.identifier]) {
679 newSelected.add(row);
680 }
681 }
682 });
683 const newSelectedArray = Array.from(newSelected.values());
684 if (
685 this.updateSelectionOnRefresh === 'onChange' &&
686 _.isEqual(this.selection.selected, newSelectedArray)
687 ) {
688 return;
689 }
690 this.selection.selected = newSelectedArray;
691 this.onSelect(this.selection);
692 }
693
694 updateExpanded() {
695 if (_.isUndefined(this.expanded) || this.updateExpandedOnRefresh === 'never') {
696 return;
697 }
698
699 const expandedId = this.expanded[this.identifier];
700 const newExpanded = _.find(this.data, (row) => expandedId === row[this.identifier]);
701
702 if (this.updateExpandedOnRefresh === 'onChange' && _.isEqual(this.expanded, newExpanded)) {
703 return;
704 }
705
706 this.expanded = newExpanded;
707 this.setExpandedRow.emit(newExpanded);
708 }
709
710 onSelect($event: any) {
711 // Ensure we do not process DOM 'select' events.
712 // https://github.com/swimlane/ngx-datatable/issues/899
713 if (_.has($event, 'selected')) {
714 this.selection.selected = $event['selected'];
715 }
716 this.updateSelection.emit(_.clone(this.selection));
717 }
718
719 private singleSelectCheck(row: any) {
720 return this.selection.selected.indexOf(row) === -1;
721 }
722
723 toggleColumn(column: CdTableColumn) {
724 const prop: TableColumnProp = column.prop;
725 const hide = !column.isHidden;
726 if (hide && this.tableColumns.length === 1) {
727 column.isHidden = true;
728 return;
729 }
730 _.find(this.localColumns, (c: CdTableColumn) => c.prop === prop).isHidden = hide;
731 this.updateColumns();
732 }
733
734 updateColumns() {
735 this.updateUserColumns();
736 this.filterHiddenColumns();
737 const sortProp = this.userConfig.sorts[0].prop;
738 if (!_.find(this.tableColumns, (c: CdTableColumn) => c.prop === sortProp)) {
739 this.userConfig.sorts = this.createSortingDefinition(this.tableColumns[0].prop);
740 }
741 this.table.recalculate();
742 this.cdRef.detectChanges();
743 }
744
745 createSortingDefinition(prop: TableColumnProp): SortPropDir[] {
746 return [
747 {
748 prop: prop,
749 dir: SortDirection.asc
750 }
751 ];
752 }
753
754 changeSorting({ sorts }: any) {
755 this.userConfig.sorts = sorts;
756 }
757
758 onClearSearch() {
759 this.search = '';
760 this.updateFilter();
761 }
762
763 onClearFilters() {
764 this.columnFilters.forEach((filter) => {
765 filter.value = undefined;
766 });
767 this.selectedFilter = _.first(this.columnFilters);
768 this.updateFilter();
769 }
770
771 updateFilter() {
772 let rows = this.columnFilters.length !== 0 ? this.doColumnFiltering() : this.data;
773
774 if (this.search.length > 0 && rows) {
775 const columns = this.localColumns.filter(
776 (c) => c.cellTransformation !== CellTemplate.sparkline
777 );
778 // update the rows
779 rows = this.subSearch(rows, TableComponent.prepareSearch(this.search), columns);
780 // Whenever the filter changes, always go back to the first page
781 this.table.offset = 0;
782 }
783
784 this.rows = rows;
785 }
786
787 subSearch(data: any[], currentSearch: string[], columns: CdTableColumn[]): any[] {
788 if (currentSearch.length === 0 || data.length === 0) {
789 return data;
790 }
791 const searchTerms: string[] = currentSearch.pop().replace(/\+/g, ' ').split(':');
792 const columnsClone = [...columns];
793 if (searchTerms.length === 2) {
794 columns = columnsClone.filter((c) => c.name.toLowerCase().indexOf(searchTerms[0]) !== -1);
795 }
796 data = this.basicDataSearch(_.last(searchTerms), data, columns);
797 // Checks if user searches for column but he is still typing
798 return this.subSearch(data, currentSearch, columnsClone);
799 }
800
801 basicDataSearch(searchTerm: string, rows: any[], columns: CdTableColumn[]) {
802 if (searchTerm.length === 0) {
803 return rows;
804 }
805 return rows.filter((row) => {
806 return (
807 columns.filter((col) => {
808 let cellValue: any = _.get(row, col.prop);
809
810 if (!_.isUndefined(col.pipe)) {
811 cellValue = col.pipe.transform(cellValue);
812 }
813 if (_.isUndefined(cellValue) || _.isNull(cellValue)) {
814 return false;
815 }
816
817 if (_.isArray(cellValue)) {
818 cellValue = cellValue.join(' ');
819 } else if (_.isNumber(cellValue) || _.isBoolean(cellValue)) {
820 cellValue = cellValue.toString();
821 }
822
823 if (_.isObjectLike(cellValue)) {
824 if (this.searchableObjects) {
825 cellValue = JSON.stringify(cellValue);
826 } else {
827 return false;
828 }
829 }
830
831 return cellValue.toLowerCase().indexOf(searchTerm) !== -1;
832 }).length > 0
833 );
834 });
835 }
836
837 getRowClass() {
838 // Return the function used to populate a row's CSS classes.
839 return () => {
840 return {
841 clickable: !_.isUndefined(this.selectionType)
842 };
843 };
844 }
845
846 toggleExpandRow(row: any, isExpanded: boolean, event: any) {
847 event.stopPropagation();
848 if (!isExpanded) {
849 // If current row isn't expanded, collapse others
850 this.expanded = row;
851 this.table.rowDetail.collapseAllRows();
852 this.setExpandedRow.emit(row);
853 } else {
854 // If all rows are closed, emit undefined
855 this.expanded = undefined;
856 this.setExpandedRow.emit(undefined);
857 }
858 this.table.rowDetail.toggleExpandRow(row);
859 }
860 }