diff options
Diffstat (limited to 'src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts')
-rw-r--r-- | src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts | 927 |
1 files changed, 927 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts new file mode 100644 index 000000000..6a37468c2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts @@ -0,0 +1,927 @@ +import { + AfterContentChecked, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + PipeTransform, + SimpleChanges, + TemplateRef, + ViewChild +} from '@angular/core'; + +import { + DatatableComponent, + getterForProp, + SortDirection, + SortPropDir, + TableColumnProp +} from '@swimlane/ngx-datatable'; +import _ from 'lodash'; +import { Observable, of, Subject, Subscription } from 'rxjs'; + +import { TableStatus } from '~/app/shared/classes/table-status'; +import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; +import { Icons } from '~/app/shared/enum/icons.enum'; +import { CdTableColumn } from '~/app/shared/models/cd-table-column'; +import { CdTableColumnFilter } from '~/app/shared/models/cd-table-column-filter'; +import { CdTableColumnFiltersChange } from '~/app/shared/models/cd-table-column-filters-change'; +import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context'; +import { PageInfo } from '~/app/shared/models/cd-table-paging'; +import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; +import { CdUserConfig } from '~/app/shared/models/cd-user-config'; +import { TimerService } from '~/app/shared/services/timer.service'; + +@Component({ + selector: 'cd-table', + templateUrl: './table.component.html', + styleUrls: ['./table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TableComponent implements AfterContentChecked, OnInit, OnChanges, OnDestroy { + @ViewChild(DatatableComponent, { static: true }) + table: DatatableComponent; + @ViewChild('tableCellBoldTpl', { static: true }) + tableCellBoldTpl: TemplateRef<any>; + @ViewChild('sparklineTpl', { static: true }) + sparklineTpl: TemplateRef<any>; + @ViewChild('routerLinkTpl', { static: true }) + routerLinkTpl: TemplateRef<any>; + @ViewChild('checkIconTpl', { static: true }) + checkIconTpl: TemplateRef<any>; + @ViewChild('perSecondTpl', { static: true }) + perSecondTpl: TemplateRef<any>; + @ViewChild('executingTpl', { static: true }) + executingTpl: TemplateRef<any>; + @ViewChild('classAddingTpl', { static: true }) + classAddingTpl: TemplateRef<any>; + @ViewChild('badgeTpl', { static: true }) + badgeTpl: TemplateRef<any>; + @ViewChild('mapTpl', { static: true }) + mapTpl: TemplateRef<any>; + @ViewChild('truncateTpl', { static: true }) + truncateTpl: TemplateRef<any>; + @ViewChild('rowDetailsTpl', { static: true }) + rowDetailsTpl: TemplateRef<any>; + + // This is the array with the items to be shown. + @Input() + data: any[]; + // Each item -> { prop: 'attribute name', name: 'display name' } + @Input() + columns: CdTableColumn[]; + // Each item -> { prop: 'attribute name', dir: 'asc'||'desc'} + @Input() + sorts?: SortPropDir[]; + // Method used for setting column widths. + @Input() + columnMode? = 'flex'; + // Display only actions in header (make sure to disable toolHeader) and use ".only-table-actions" + @Input() + onlyActionHeader? = false; + // Display the tool header, including reload button, pagination and search fields? + @Input() + toolHeader? = true; + // Display search field inside tool header? + @Input() + searchField? = true; + // Display the table header? + @Input() + header? = true; + // Display the table footer? + @Input() + footer? = true; + // Page size to show. Set to 0 to show unlimited number of rows. + @Input() + limit? = 10; + @Input() + maxLimit? = 9999; + // Has the row details? + @Input() + hasDetails = false; + + /** + * Auto reload time in ms - per default every 5s + * You can set it to 0, undefined or false to disable the auto reload feature in order to + * trigger 'fetchData' if the reload button is clicked. + * You can set it to a negative number to, on top of disabling the auto reload, + * prevent triggering fetchData when initializing the table. + */ + @Input() + autoReload = 5000; + + // Which row property is unique for a row. If the identifier is not specified in any + // column, then the property name of the first column is used. Defaults to 'id'. + @Input() + identifier = 'id'; + // If 'true', then the specified identifier is used anyway, although it is not specified + // in any column. Defaults to 'false'. + @Input() + forceIdentifier = false; + // Allows other components to specify which type of selection they want, + // e.g. 'single' or 'multi'. + @Input() + selectionType: string = undefined; + // By default selected item details will be updated on table refresh, if data has changed + @Input() + updateSelectionOnRefresh: 'always' | 'never' | 'onChange' = 'onChange'; + // By default expanded item details will be updated on table refresh, if data has changed + @Input() + updateExpandedOnRefresh: 'always' | 'never' | 'onChange' = 'onChange'; + + @Input() + autoSave = true; + + // Enable this in order to search through the JSON of any used object. + @Input() + searchableObjects = false; + + // Only needed to set if the classAddingTpl is used + @Input() + customCss?: { [css: string]: number | string | ((any: any) => boolean) }; + + // Columns that aren't displayed but can be used as filters + @Input() + extraFilterableColumns: CdTableColumn[] = []; + + @Input() + status = new TableStatus(); + + // Support server-side pagination/sorting/etc. + @Input() + serverSide = false; + + /* + Only required when serverSide is enabled. + It should be provided by the server via "X-Total-Count" HTTP Header + */ + @Input() + count = 0; + + /** + * Should be a function to update the input data if undefined nothing will be triggered + * + * Sometimes it's useful to only define fetchData once. + * Example: + * Usage of multiple tables with data which is updated by the same function + * What happens: + * The function is triggered through one table and all tables will update + */ + @Output() + fetchData = new EventEmitter<CdTableFetchDataContext>(); + + /** + * This should be defined if you need access to the selection object. + * + * Each time the table selection changes, this will be triggered and + * the new selection object will be sent. + * + * @memberof TableComponent + */ + @Output() + updateSelection = new EventEmitter(); + + @Output() + setExpandedRow = new EventEmitter(); + + /** + * This should be defined if you need access to the applied column filters. + * + * Each time the column filters changes, this will be triggered and + * the column filters change event will be sent. + * + * @memberof TableComponent + */ + @Output() columnFiltersChanged = new EventEmitter<CdTableColumnFiltersChange>(); + + /** + * Use this variable to access the selected row(s). + */ + selection = new CdTableSelection(); + + /** + * Use this variable to access the expanded row + */ + expanded: any = undefined; + + /** + * To prevent making changes to the original columns list, that might change + * how the table is renderer a second time, we now clone that list into a + * local variable and only use the clone. + */ + localColumns: CdTableColumn[]; + tableColumns: CdTableColumn[]; + icons = Icons; + cellTemplates: { + [key: string]: TemplateRef<any>; + } = {}; + search = ''; + rows: any[] = []; + loadingIndicator = true; + paginationClasses = { + pagerLeftArrow: Icons.leftArrowDouble, + pagerRightArrow: Icons.rightArrowDouble, + pagerPrevious: Icons.leftArrow, + pagerNext: Icons.rightArrow + }; + userConfig: CdUserConfig = {}; + tableName: string; + localStorage = window.localStorage; + private saveSubscriber: Subscription; + private reloadSubscriber: Subscription; + private updating = false; + + // Internal variable to check if it is necessary to recalculate the + // table columns after the browser window has been resized. + private currentWidth: number; + + columnFilters: CdTableColumnFilter[] = []; + selectedFilter: CdTableColumnFilter; + get columnFiltered(): boolean { + return _.some(this.columnFilters, (filter) => { + return filter.value !== undefined; + }); + } + + constructor( + // private ngZone: NgZone, + private cdRef: ChangeDetectorRef, + private timerService: TimerService + ) {} + + static prepareSearch(search: string) { + search = search.toLowerCase().replace(/,/g, ''); + if (search.match(/['"][^'"]+['"]/)) { + search = search.replace(/['"][^'"]+['"]/g, (match: string) => { + return match.replace(/(['"])([^'"]+)(['"])/g, '$2').replace(/ /g, '+'); + }); + } + return search.split(' ').filter((word) => word); + } + + ngOnInit() { + this.localColumns = _.clone(this.columns); + // debounce reloadData method so that search doesn't run api requests + // for every keystroke + if (this.serverSide) { + this.reloadData = _.debounce(this.reloadData, 1000); + } + + // ngx-datatable triggers calculations each time mouse enters a row, + // this will prevent that. + this.table.element.addEventListener('mouseenter', (e) => e.stopPropagation()); + this._addTemplates(); + if (!this.sorts) { + // Check whether the specified identifier exists. + const exists = _.findIndex(this.localColumns, ['prop', this.identifier]) !== -1; + // Auto-build the sorting configuration. If the specified identifier doesn't exist, + // then use the property of the first column. + this.sorts = this.createSortingDefinition( + exists ? this.identifier : this.localColumns[0].prop + '' + ); + // If the specified identifier doesn't exist and it is not forced to use it anyway, + // then use the property of the first column. + if (!exists && !this.forceIdentifier) { + this.identifier = this.localColumns[0].prop + ''; + } + } + + this.initUserConfig(); + this.localColumns.forEach((c) => { + if (c.cellTransformation) { + c.cellTemplate = this.cellTemplates[c.cellTransformation]; + } + if (!c.flexGrow) { + c.flexGrow = c.prop + '' === this.identifier ? 1 : 2; + } + if (!c.resizeable) { + c.resizeable = false; + } + }); + + this.initExpandCollapseColumn(); // If rows have details, add a column to expand or collapse the rows + this.initCheckboxColumn(); + this.filterHiddenColumns(); + this.initColumnFilters(); + this.updateColumnFilterOptions(); + // Notify all subscribers to reset their current selection. + this.updateSelection.emit(new CdTableSelection()); + // Load the data table content every N ms or at least once. + // Force showing the loading indicator if there are subscribers to the fetchData + // event. This is necessary because it has been set to False in useData() when + // this method was triggered by ngOnChanges(). + if (this.fetchData.observers.length > 0) { + this.loadingIndicator = true; + } + if (_.isInteger(this.autoReload) && this.autoReload > 0) { + this.reloadSubscriber = this.timerService + .get(() => of(0), this.autoReload) + .subscribe(() => { + this.reloadData(); + }); + } else if (!this.autoReload) { + this.reloadData(); + } else { + this.useData(); + } + + if (this.selectionType === 'single') { + this.table.selectCheck = this.singleSelectCheck.bind(this); + } + } + + initUserConfig() { + if (this.autoSave) { + this.tableName = this._calculateUniqueTableName(this.localColumns); + this._loadUserConfig(); + this._initUserConfigAutoSave(); + } + if (!this.userConfig.limit) { + this.userConfig.limit = this.limit; + } + if (!(this.userConfig.offset >= 0)) { + this.userConfig.offset = this.table.offset; + } + if (!this.userConfig.search) { + this.userConfig.search = this.search; + } + if (!this.userConfig.sorts) { + this.userConfig.sorts = this.sorts; + } + if (!this.userConfig.columns) { + this.updateUserColumns(); + } else { + this.userConfig.columns.forEach((col) => { + for (let i = 0; i < this.localColumns.length; i++) { + if (this.localColumns[i].prop === col.prop) { + this.localColumns[i].isHidden = col.isHidden; + } + } + }); + } + } + + _calculateUniqueTableName(columns: any[]) { + const stringToNumber = (s: string) => { + if (!_.isString(s)) { + return 0; + } + let result = 0; + for (let i = 0; i < s.length; i++) { + result += s.charCodeAt(i) * i; + } + return result; + }; + return columns + .reduce( + (result, value, index) => + (stringToNumber(value.prop) + stringToNumber(value.name)) * (index + 1) + result, + 0 + ) + .toString(); + } + + _loadUserConfig() { + const loaded = this.localStorage.getItem(this.tableName); + if (loaded) { + this.userConfig = JSON.parse(loaded); + } + } + + _initUserConfigAutoSave() { + const source: Observable<any> = new Observable(this._initUserConfigProxy.bind(this)); + this.saveSubscriber = source.subscribe(this._saveUserConfig.bind(this)); + } + + _initUserConfigProxy(observer: Subject<any>) { + this.userConfig = new Proxy(this.userConfig, { + set(config, prop: string, value) { + config[prop] = value; + observer.next(config); + return true; + } + }); + } + + _saveUserConfig(config: any) { + this.localStorage.setItem(this.tableName, JSON.stringify(config)); + } + + updateUserColumns() { + this.userConfig.columns = this.localColumns.map((c) => ({ + prop: c.prop, + name: c.name, + isHidden: !!c.isHidden + })); + } + + /** + * Add a column containing a checkbox if selectionType is 'multiClick'. + */ + initCheckboxColumn() { + if (this.selectionType === 'multiClick') { + this.localColumns.unshift({ + prop: undefined, + resizeable: false, + sortable: false, + draggable: false, + checkboxable: true, + canAutoResize: false, + cellClass: 'cd-datatable-checkbox', + width: 30 + }); + } + } + + /** + * Add a column to expand and collapse the table row if it 'hasDetails' + */ + initExpandCollapseColumn() { + if (this.hasDetails) { + this.localColumns.unshift({ + prop: undefined, + resizeable: false, + sortable: false, + draggable: false, + isHidden: false, + canAutoResize: false, + cellClass: 'cd-datatable-expand-collapse', + width: 40, + cellTemplate: this.rowDetailsTpl + }); + } + } + + filterHiddenColumns() { + this.tableColumns = this.localColumns.filter((c) => !c.isHidden); + } + + initColumnFilters() { + let filterableColumns = _.filter(this.localColumns, { filterable: true }); + filterableColumns = [...filterableColumns, ...this.extraFilterableColumns]; + this.columnFilters = filterableColumns.map((col: CdTableColumn) => { + return { + column: col, + options: [], + value: col.filterInitValue + ? this.createColumnFilterOption(col.filterInitValue, col.pipe) + : undefined + }; + }); + this.selectedFilter = _.first(this.columnFilters); + } + + private createColumnFilterOption( + value: any, + pipe?: PipeTransform + ): { raw: string; formatted: string } { + return { + raw: _.toString(value), + formatted: pipe ? pipe.transform(value) : _.toString(value) + }; + } + + updateColumnFilterOptions() { + // update all possible values in a column + this.columnFilters.forEach((filter) => { + let values: any[] = []; + + if (_.isUndefined(filter.column.filterOptions)) { + // only allow types that can be easily converted into string + const pre = _.filter(_.map(this.data, filter.column.prop), (v) => { + return (_.isString(v) && v !== '') || _.isBoolean(v) || _.isFinite(v) || _.isDate(v); + }); + values = _.sortedUniq(pre.sort()); + } else { + values = filter.column.filterOptions; + } + + const options = values.map((v) => this.createColumnFilterOption(v, filter.column.pipe)); + + // In case a previous value is not available anymore + if (filter.value && _.isUndefined(_.find(options, { raw: filter.value.raw }))) { + filter.value = undefined; + } + + filter.options = options; + }); + } + + onSelectFilter(filter: CdTableColumnFilter) { + this.selectedFilter = filter; + } + + onChangeFilter(filter: CdTableColumnFilter, option?: { raw: string; formatted: string }) { + filter.value = _.isEqual(filter.value, option) ? undefined : option; + this.updateFilter(); + } + + doColumnFiltering() { + const appliedFilters: CdTableColumnFiltersChange['filters'] = []; + let data = [...this.data]; + let dataOut: any[] = []; + this.columnFilters.forEach((filter) => { + if (filter.value === undefined) { + return; + } + appliedFilters.push({ + name: filter.column.name, + prop: filter.column.prop, + value: filter.value + }); + // Separate data to filtered and filtered-out parts. + const parts = _.partition(data, (row) => { + // Use getter from ngx-datatable to handle props like 'sys_api.size' + const valueGetter = getterForProp(filter.column.prop); + const value = valueGetter(row, filter.column.prop); + if (_.isUndefined(filter.column.filterPredicate)) { + // By default, test string equal + return `${value}` === filter.value.raw; + } else { + // Use custom function to filter + return filter.column.filterPredicate(row, filter.value.raw); + } + }); + data = parts[0]; + dataOut = [...dataOut, ...parts[1]]; + }); + + this.columnFiltersChanged.emit({ + filters: appliedFilters, + data: data, + dataOut: dataOut + }); + + // Remove the selection if previously-selected rows are filtered out. + _.forEach(this.selection.selected, (selectedItem) => { + if (_.find(data, { [this.identifier]: selectedItem[this.identifier] }) === undefined) { + this.selection = new CdTableSelection(); + this.onSelect(this.selection); + } + }); + return data; + } + + ngOnDestroy() { + if (this.reloadSubscriber) { + this.reloadSubscriber.unsubscribe(); + } + if (this.saveSubscriber) { + this.saveSubscriber.unsubscribe(); + } + } + + ngAfterContentChecked() { + // If the data table is not visible, e.g. another tab is active, and the + // browser window gets resized, the table and its columns won't get resized + // automatically if the tab gets visible again. + // https://github.com/swimlane/ngx-datatable/issues/193 + // https://github.com/swimlane/ngx-datatable/issues/193#issuecomment-329144543 + if (this.table && this.table.element.clientWidth !== this.currentWidth) { + this.currentWidth = this.table.element.clientWidth; + // Recalculate the sizes of the grid. + this.table.recalculate(); + // Mark the datatable as changed, Angular's change-detection will + // do the rest for us => the grid will be redrawn. + // Note, the ChangeDetectorRef variable is private, so we need to + // use this workaround to access it and make TypeScript happy. + const cdRef = _.get(this.table, 'cd'); + cdRef.markForCheck(); + } + } + + _addTemplates() { + this.cellTemplates.bold = this.tableCellBoldTpl; + this.cellTemplates.checkIcon = this.checkIconTpl; + this.cellTemplates.sparkline = this.sparklineTpl; + this.cellTemplates.routerLink = this.routerLinkTpl; + this.cellTemplates.perSecond = this.perSecondTpl; + this.cellTemplates.executing = this.executingTpl; + this.cellTemplates.classAdding = this.classAddingTpl; + this.cellTemplates.badge = this.badgeTpl; + this.cellTemplates.map = this.mapTpl; + this.cellTemplates.truncate = this.truncateTpl; + } + + useCustomClass(value: any): string { + if (!this.customCss) { + throw new Error('Custom classes are not set!'); + } + const classes = Object.keys(this.customCss); + const css = Object.values(this.customCss) + .map((v, i) => ((_.isFunction(v) && v(value)) || v === value) && classes[i]) + .filter((x) => x) + .join(' '); + return _.isEmpty(css) ? undefined : css; + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.data && changes.data.currentValue) { + this.useData(); + } + } + + setLimit(e: any) { + const value = Number(e.target.value); + if (value > 0) { + if (this.maxLimit && value > this.maxLimit) { + this.userConfig.limit = this.maxLimit; + // change input field to maxLimit + e.srcElement.value = this.maxLimit; + } else { + this.userConfig.limit = value; + } + } + if (this.serverSide) { + this.reloadData(); + } + } + + reloadData() { + if (!this.updating) { + this.status = new TableStatus(); + const context = new CdTableFetchDataContext(() => { + // Do we have to display the error panel? + if (!!context.errorConfig.displayError) { + this.status = new TableStatus('danger', $localize`Failed to load data.`); + } + // Force data table to show no data? + if (context.errorConfig.resetData) { + this.data = []; + } + // Stop the loading indicator and reset the data table + // to the correct state. + this.useData(); + }); + context.pageInfo.offset = this.userConfig.offset; + context.pageInfo.limit = this.userConfig.limit; + context.search = this.userConfig.search; + if (this.userConfig.sorts?.length) { + const sort = this.userConfig.sorts[0]; + context.sort = `${sort.dir === 'desc' ? '-' : '+'}${sort.prop}`; + } + this.fetchData.emit(context); + this.updating = true; + } + } + + refreshBtn() { + this.loadingIndicator = true; + this.reloadData(); + } + + changePage(pageInfo: PageInfo) { + this.userConfig.offset = pageInfo.offset; + this.userConfig.limit = pageInfo.limit; + if (this.serverSide) { + this.reloadData(); + } + } + rowIdentity() { + return (row: any) => { + const id = row[this.identifier]; + if (_.isUndefined(id)) { + throw new Error(`Wrong identifier "${this.identifier}" -> "${id}"`); + } + return id; + }; + } + + useData() { + if (!this.data) { + return; // Wait for data + } + this.updateColumnFilterOptions(); + this.updateFilter(); + this.reset(); + this.updateSelected(); + this.updateExpanded(); + } + + /** + * Reset the data table to correct state. This includes: + * - Disable loading indicator + * - Reset 'Updating' flag + */ + reset() { + this.loadingIndicator = false; + this.updating = false; + } + + /** + * After updating the data, we have to update the selected items + * because details may have changed, + * or some selected items may have been removed. + */ + updateSelected() { + if (this.updateSelectionOnRefresh === 'never') { + return; + } + const newSelected = new Set(); + this.selection.selected.forEach((selectedItem) => { + for (const row of this.data) { + if (selectedItem[this.identifier] === row[this.identifier]) { + newSelected.add(row); + } + } + }); + const newSelectedArray = Array.from(newSelected.values()); + if ( + this.updateSelectionOnRefresh === 'onChange' && + _.isEqual(this.selection.selected, newSelectedArray) + ) { + return; + } + this.selection.selected = newSelectedArray; + this.onSelect(this.selection); + } + + updateExpanded() { + if (_.isUndefined(this.expanded) || this.updateExpandedOnRefresh === 'never') { + return; + } + + const expandedId = this.expanded[this.identifier]; + const newExpanded = _.find(this.data, (row) => expandedId === row[this.identifier]); + + if (this.updateExpandedOnRefresh === 'onChange' && _.isEqual(this.expanded, newExpanded)) { + return; + } + + this.expanded = newExpanded; + this.setExpandedRow.emit(newExpanded); + } + + onSelect($event: any) { + // Ensure we do not process DOM 'select' events. + // https://github.com/swimlane/ngx-datatable/issues/899 + if (_.has($event, 'selected')) { + this.selection.selected = $event['selected']; + } + this.updateSelection.emit(_.clone(this.selection)); + } + + private singleSelectCheck(row: any) { + return this.selection.selected.indexOf(row) === -1; + } + + toggleColumn(column: CdTableColumn) { + const prop: TableColumnProp = column.prop; + const hide = !column.isHidden; + if (hide && this.tableColumns.length === 1) { + column.isHidden = true; + return; + } + _.find(this.localColumns, (c: CdTableColumn) => c.prop === prop).isHidden = hide; + this.updateColumns(); + } + + updateColumns() { + this.updateUserColumns(); + this.filterHiddenColumns(); + const sortProp = this.userConfig.sorts[0].prop; + if (!_.find(this.tableColumns, (c: CdTableColumn) => c.prop === sortProp)) { + this.userConfig.sorts = this.createSortingDefinition(this.tableColumns[0].prop); + } + this.table.recalculate(); + this.cdRef.detectChanges(); + } + + createSortingDefinition(prop: TableColumnProp): SortPropDir[] { + return [ + { + prop: prop, + dir: SortDirection.asc + } + ]; + } + + changeSorting({ sorts }: any) { + this.userConfig.sorts = sorts; + if (this.serverSide) { + this.userConfig.offset = 0; + this.reloadData(); + } + } + + onClearSearch() { + this.search = ''; + this.updateFilter(); + } + + onClearFilters() { + this.columnFilters.forEach((filter) => { + filter.value = undefined; + }); + this.selectedFilter = _.first(this.columnFilters); + this.updateFilter(); + } + + updateFilter() { + if (this.serverSide) { + if (this.userConfig.search !== this.search) { + // if we don't go back to the first page it will try load + // a page which could not exists with an especific search + this.userConfig.offset = 0; + this.userConfig.limit = this.limit; + this.userConfig.search = this.search; + this.updating = false; + this.reloadData(); + } + this.rows = this.data; + } else { + let rows = this.columnFilters.length !== 0 ? this.doColumnFiltering() : this.data; + + if (this.search.length > 0 && rows) { + const columns = this.localColumns.filter( + (c) => c.cellTransformation !== CellTemplate.sparkline + ); + // update the rows + rows = this.subSearch(rows, TableComponent.prepareSearch(this.search), columns); + // Whenever the filter changes, always go back to the first page + this.table.offset = 0; + } + + this.rows = rows; + } + } + + subSearch(data: any[], currentSearch: string[], columns: CdTableColumn[]): any[] { + if (currentSearch.length === 0 || data.length === 0) { + return data; + } + const searchTerms: string[] = currentSearch.pop().replace(/\+/g, ' ').split(':'); + const columnsClone = [...columns]; + if (searchTerms.length === 2) { + columns = columnsClone.filter((c) => c.name.toLowerCase().indexOf(searchTerms[0]) !== -1); + } + data = this.basicDataSearch(_.last(searchTerms), data, columns); + // Checks if user searches for column but he is still typing + return this.subSearch(data, currentSearch, columnsClone); + } + + basicDataSearch(searchTerm: string, rows: any[], columns: CdTableColumn[]) { + if (searchTerm.length === 0) { + return rows; + } + return rows.filter((row) => { + return ( + columns.filter((col) => { + let cellValue: any = _.get(row, col.prop); + + if (!_.isUndefined(col.pipe)) { + cellValue = col.pipe.transform(cellValue); + } + if (_.isUndefined(cellValue) || _.isNull(cellValue)) { + return false; + } + + if (_.isArray(cellValue)) { + cellValue = cellValue.join(' '); + } else if (_.isNumber(cellValue) || _.isBoolean(cellValue)) { + cellValue = cellValue.toString(); + } + + if (_.isObjectLike(cellValue)) { + if (this.searchableObjects) { + cellValue = JSON.stringify(cellValue); + } else { + return false; + } + } + + return cellValue.toLowerCase().indexOf(searchTerm) !== -1; + }).length > 0 + ); + }); + } + + getRowClass() { + // Return the function used to populate a row's CSS classes. + return () => { + return { + clickable: !_.isUndefined(this.selectionType) + }; + }; + } + + toggleExpandRow(row: any, isExpanded: boolean, event: any) { + event.stopPropagation(); + if (!isExpanded) { + // If current row isn't expanded, collapse others + this.expanded = row; + this.table.rowDetail.collapseAllRows(); + this.setExpandedRow.emit(row); + } else { + // If all rows are closed, emit undefined + this.expanded = undefined; + this.setExpandedRow.emit(undefined); + } + this.table.rowDetail.toggleExpandRow(row); + } +} |