UNPKG

@nova-ui/dashboards

Version:

Nova Dashboards is a framework designed to provide feature developers with a common solution for presenting data coming from various sources within a single view, as well as a set of predefined widget visualizations that are 100% configuration-driven and

633 lines 28.2 kB
"use strict"; // © 2022 SolarWinds Worldwide, LLC. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. Object.defineProperty(exports, "__esModule", { value: true }); exports.TableWidgetComponent = void 0; const tslib_1 = require("tslib"); const scrolling_1 = require("@angular/cdk/scrolling"); const core_1 = require("@angular/core"); const get_1 = tslib_1.__importDefault(require("lodash/get")); const isEqual_1 = tslib_1.__importDefault(require("lodash/isEqual")); const omit_1 = tslib_1.__importDefault(require("lodash/omit")); // eslint-disable-next-line import/no-deprecated const rxjs_1 = require("rxjs"); // eslint-disable-next-line import/no-deprecated const operators_1 = require("rxjs/operators"); const bits_1 = require("@nova-ui/bits"); const pizzagna_service_1 = require("../../pizzagna/services/pizzagna.service"); const table_formatter_registry_service_1 = require("../../services/table-formatter-registry.service"); const types_1 = require("../../services/types"); const widget_configuration_service_1 = require("../../services/widget-configuration.service"); const types_2 = require("../../types"); const paginator_feature_addon_service_1 = require("./addons/paginator-feature-addon.service"); const search_feature_addon_service_1 = require("./addons/search-feature-addon.service"); const virtual_scroll_feature_addon_service_1 = require("./addons/virtual-scroll-feature-addon.service"); const types_3 = require("./types"); /** * @ignore */ let TableWidgetComponent = class TableWidgetComponent { static { this.lateLoadKey = "TableWidgetComponent"; } constructor(eventBus, dataSource, widgetConfigurationService, changeDetector, pizzagnaService, viewportManager, zone, el, logger, searchAddon, paginatorAddon, virtualScrollAddon, formattersRegistryService, selectorService) { this.eventBus = eventBus; this.dataSource = dataSource; this.widgetConfigurationService = widgetConfigurationService; this.changeDetector = changeDetector; this.pizzagnaService = pizzagnaService; this.viewportManager = viewportManager; this.zone = zone; this.el = el; this.logger = logger; this.searchAddon = searchAddon; this.paginatorAddon = paginatorAddon; this.virtualScrollAddon = virtualScrollAddon; this.formattersRegistryService = formattersRegistryService; this.selectorService = selectorService; // 70 px stands for widget header and margins this.indentFromTop = 70; this.sortable = true; this.delayedMousePresenceDetectionEnabled = true; this.tableData = []; this.columns = []; this.columnsWidthMap = new Map(); this.scrollType = types_3.ScrollType.virtual; this.isSearchEnabled = false; this.searchTerm$ = new rxjs_1.Subject(); this.onDestroy$ = new rxjs_1.Subject(); this.tableUpdate$ = new rxjs_1.Subject(); this.mousePresent$ = new rxjs_1.BehaviorSubject(false); this.rowHeight = 24; this.selection = { isAllPages: false, include: [], exclude: [], }; this.totalPages = 0; this.lastPageFetched = 0; this.isBusy = this.lastPageFetched !== this.totalPages; this.sortableSet = {}; this.defaultColumnAlignment = "left"; this.idle = false; this._scrollBuffer = 0; } get headerTooltipsEnabled() { // Note: If tooltip state is not provided treat is as true; return this.configuration.headerTooltipsEnabled ?? true; } set scrollBuffer(value) { if (value > 100 || value < 0) { throw new Error("Invalid scroll buffer provided. Required range 1..100"); } this._scrollBuffer = value; } get range() { return this.getTableScrollRange(); } set range(value) { this._range = value; } get interactive() { return (this.configuration?.interactive ?? this.dataSource?.features?.getFeatureConfig(types_2.WellKnownDataSourceFeatures.Interactivity)?.enabled ?? false); } get clickableRow() { const configuration = this.configuration?.selectionConfiguration; return (!!configuration?.enabled && (configuration.clickableRow || configuration.selectionMode === bits_1.TableSelectionMode.Single)); } get hasVirtualScroll() { return this.scrollType === types_3.ScrollType.virtual; } get hasPaginator() { return this.scrollType === types_3.ScrollType.paginator; } ngOnChanges(changes) { if (changes.dataFields) { if (this.dataFields?.length) { this.setSortableSet(); } } if (changes.widgetData || changes.configuration || changes.dataFields) { this.updateTable(); // Note: CDK Viewport does not have a proper mechanism to identify the changes and trigger checkViewportSize // we have to do it manually. Using optional chaining because vscrollViewport can be unavailable at initialization. // Related issue: https://github.com/angular/components/issues/10117 this.vscrollViewport?.checkViewportSize(); } if (changes.configuration) { this.resolveSortBy(changes); this.resolveScrollType(changes); this.resolveSearch(changes); } if (changes.totalItems) { this.totalPages = Math.floor((this.totalItems ?? 0) / this.range); } } resolveScrollType(changes) { // Since removing "hasVirtualScroll" from configuration would cause breaking changes, it is used as primary source of truth const newHasVirtualScroll = (0, get_1.default)(changes, "configuration.currentValue.hasVirtualScroll", true); const scrollType = (0, get_1.default)(changes, "configuration.currentValue.scrollType", types_3.ScrollType.virtual); this.scrollType = newHasVirtualScroll ? types_3.ScrollType.virtual : scrollType; this.initPrefetchAddon(); if (this.scrollTypeChanged(changes.configuration)) { this.dataSource.applyFilters(); } } ngOnInit() { if (this.dataSource) { // Since the sortFilter is not initialized, we have to retrieve and set the correct filter value from the configuration before registering it this.setSortFilter(); this.registerSorter(); } } ngAfterViewInit() { if (!this.dataSource) { return; } this.dataSource.busy ?.pipe( // eslint-disable-next-line import/no-deprecated (0, operators_1.tap)((isBusy) => (this.isBusy = isBusy)), (0, operators_1.takeUntil)(this.onDestroy$)) .subscribe(); this.initPrefetchAddon(); this.searchAddon.initWidget(this); const tableHeightChanged$ = this.eventBus .getStream(types_1.WIDGET_RESIZE) .pipe((0, operators_1.filter)((event) => event.payload?.widgetId?.toString() === this.widgetConfigurationService.getWidget().id), // eslint-disable-next-line import/no-deprecated (0, operators_1.map)((event) => event.payload?.height ?? 0)); // subscribing to widget resize event from dashboard and update virtual scroll viewport size tableHeightChanged$ .pipe((0, operators_1.takeUntil)(this.onDestroy$)) .subscribe((height) => { this.tableContainerHeight = height - this.indentFromTop; this.vscrollViewport?.checkViewportSize(); this.changeDetector.detectChanges(); }); // Note: Secondary stream used to trigger widget ready event when table is properly displayed and we can start // loading data for our virtual scroll // setTimeout is needed because this.el.nativeElement.getBoundingClientRect().height is 0 when it switches components from // fallback component to table widget and it needs to have height setTimeout(() => { // eslint-disable-next-line import/no-deprecated (0, rxjs_1.merge)((0, rxjs_1.of)(this.range * this.rowHeight), tableHeightChanged$, (0, rxjs_1.of)(this.el.nativeElement.getBoundingClientRect().height)) .pipe((0, operators_1.filter)((value) => !!value), (0, operators_1.take)(1), // eslint-disable-next-line import/no-deprecated (0, operators_1.tap)((value) => { this.tableWidgetHeight = value; if (this.hasVirtualScroll) { this.virtualScrollAddon.subscribeToVirtualScroll(); } this.eventBus.getStream(types_1.WIDGET_READY).next({}); })) .subscribe(); }); this.eventBus .getStream(types_1.CHANGE_SELECTION) .pipe((0, operators_1.takeUntil)(this.onDestroy$)) .subscribe(({ payload: selection }) => { if (selection) { this.selection = selection; } else { this.logger.warn(`widget CHANGE_SELECTION, ${this.componentId} has received the incorrect selection object`); } }); } ngOnDestroy() { this.onDestroy$.next(); this.onDestroy$.complete(); this.tableUpdate$.complete(); this.searchTerm$?.complete(); // erase the error status when this component gets destroyed to prevent an error status leak when changing data sources this.pizzagnaService.setProperty({ componentId: "bodyContent", propertyPath: ["fallbackKey"], pizzagnaKey: types_2.PizzagnaLayer.Data, }, undefined); } /** Checks if table should be displayed */ shouldDisplayTable() { const columnsCondition = this.columns.length > 0; const dataCondition = this.tableData?.length > 0; return this.isSearchEnabled || (this.hasVirtualScroll && (this.isBusy || this.idle)) ? columnsCondition : columnsCondition && dataCondition; } dataTrackBy() { const configuration = this?.configuration?.selectionConfiguration; const trackByProperty = (configuration?.enabled && configuration.trackByProperty) || "id"; // we are returning a function here because the scope of "this" changes once it's passed // to the table component and are therefore unable to access the configuration in its body return (index, item) => { // selection uses a set of the columns in prior of selection correct data id we need to use record if (item["__record"]) { item = item["__record"]; } return item ? item[trackByProperty] : index; }; } columnTrackBy(index, item) { return item.id; } /** * Handles updating of columns. * @param configuration */ updateColumns(configuration) { this.headers = configuration.columns .filter((item) => item.isActive) .map((item) => item.id); const allColumnsHaveWidthSpecified = configuration.columns.every((column) => Boolean(column.width)); const columns = configuration.columns.map((column, index, array) => { this.columnsWidthMap.set(column.id, column.width); if (allColumnsHaveWidthSpecified) { const lastColumn = array[array.length - 1]; this.columnsWidthMap.set(lastColumn.id, undefined); } return { ...column, sortable: this.sortableSet[column?.formatter?.properties?.dataFieldIds?.value] ?? true, }; }); if (columns.length > 0 && allColumnsHaveWidthSpecified) { this.logger.warn(`Cannot set width for all columns. Resetting last column width.`); } if (this.columns.length !== columns.length) { this.columns = [...columns]; } else { // This Object.assign is important. It helps to track changes in column header names this.columns.forEach((c, i) => { Object.assign(c, columns[i], {}); }); } } /** * Takes widgetData from back-end, columns which are provided in table widget config and dataFields and maps them to data-format * which is acceptable by table component. * Also it can merge multiple data fields into one column. If you provide more than one dataFieldId in your * widget config, they will be merged to an object with needed data. This data can be passed to formatter and displayed * in one column. * @param widgetData * @param columns * @param dataFields * @returns any[] */ mapTableData(widgetData, columns, dataFields) { if (!dataFields || dataFields.length === 0) { this.logger.warn("There are no data fields defined, so table data cannot be displayed."); return []; } return widgetData.map((record) => { const row = columns.reduce((result, column) => { const dataFieldIds = column.formatter?.properties?.dataFieldIds; if (!dataFieldIds) { return result; } const data = Object.keys(dataFieldIds).reduce((mapping, next) => { mapping[next] = record[dataFieldIds[next]]; return mapping; }, {}); result[column.id] = { data, ...(0, omit_1.default)(column.formatter?.properties, "dataFieldIds"), }; return result; }, {}); // we want to include original record for row interaction row.__record = record; return row; }); } /** * Handles change of sorting. Gets sorted column from columns array. * @param event */ onSortOrderChanged(event) { this.sortedColumn = event; const columnToSort = this.columns.find((column) => column.id === event.sortBy); if (!columnToSort) { return; } const dataFieldIds = columnToSort.formatter?.properties?.dataFieldIds; const sortField = dataFieldIds?.value; if (!sortField) { return; } const newSortFilter = { sortBy: sortField, direction: event.direction, }; if ((0, isEqual_1.default)(newSortFilter, this.sortFilter)) { return; } this.sortFilter = newSortFilter; // Note: Since we're relying on the CDK Viewport internal logic to perform data load // we have to move to the start of the range for dataSource to take correct range, // if we will keep old data user can think (NUI-5333) that data is already reloaded. // So we're flushing the old data and waiting for new one to avoid confusion. this.flushTableData(); this.eventBus.getStream(types_1.REFRESH).next({}); } onSelectionChange(event) { this.selection = event; this.eventBus.getStream(types_1.SELECTED_ITEMS).next({ payload: this.selectorService.getSelectedItems(this.selection, this.widgetData, (a, b) => this.dataTrackBy()(0, a) === b), id: this.componentId, }); this.eventBus.getStream(types_1.SELECTION).next({ payload: this.selection }); } onInteraction(row, event) { if (!this.interactive || this.configuration.selectionConfiguration?.enabled) { // allowing interactive and selection at the same time would trigger both // events when clicking on a row (not checkbox/radio) return; } const ignoredSelectors = this.configuration.interactionIgnoredSelectors || bits_1.DEFAULT_INTERACTIVE_ELEMENTS; // avoid emitting events when an ignored element was clicked if (event.target.closest(ignoredSelectors.join(","))) { return; } this.eventBus .getStream(types_1.INTERACTION) .next({ payload: { data: row.__record } }); } onSearchInputChanged(searchTerm) { this.searchValue = searchTerm; this.searchTerm$.next(""); this.selection = { include: [], exclude: [], isAllPages: false, }; } getColumnAlignment(column) { // Note: In case we have provided formatters by old manner (via config) // we don't have to proceed with calculations if (!column?.formatter?.componentType || this.formattersRegistryService.isEmpty) { return this.defaultColumnAlignment; } // Note: We don't want to invoke getFormattersMap() on every change detection cycle // but only when it was changed if (this.formatters?.version !== this.formattersRegistryService.stateVersion) { this.formatters = { // Transforming array into map items: this.formattersRegistryService.getItems().reduce((prev, next) => ({ ...prev, [next.componentType]: next, }), {}), version: this.formattersRegistryService.stateVersion, }; } return (this.formatters.items[column.formatter.componentType]?.alignment || this.defaultColumnAlignment); } setSortFilter() { if (this.configuration) { const sortBy = this.configuration.sorterConfiguration?.sortBy; const columnValue = this.configuration.columns?.find((column) => column.id === sortBy)?.formatter?.properties?.dataFieldIds?.value; this.sortFilter = { direction: this.configuration.sorterConfiguration ?.descendantSorting ? bits_1.SorterDirection.descending : bits_1.SorterDirection.ascending, sortBy: columnValue, }; } } /** * Checks if column id has changed or if sorting order has changed * @param configuration */ isSortByUpdated(configuration) { const oldSorterConfiguration = configuration.previousValue?.sorterConfiguration; const newSorterConfiguration = configuration.currentValue.sorterConfiguration; const oldSortById = oldSorterConfiguration?.sortBy; const newSortById = newSorterConfiguration?.sortBy; if (!newSortById) { return Boolean(oldSortById); } const equalSortingState = oldSortById === newSortById && newSorterConfiguration.descendantSorting === oldSorterConfiguration.descendantSorting; return !equalSortingState; } /** * Updates table columns and maps table data. */ updateTable() { if (this.widgetData && this.dataFields && this.configuration.columns) { this.updateColumns(this.configuration); this.idle = false; this.tableData = this.mapTableData(this.widgetData, this.configuration.columns, this.dataFields); this.tableUpdate$.next(); this.changeDetector.detectChanges(); } } /** * Registers sorter filter. */ registerSorter() { this.dataSource.registerComponent({ sorter: { componentInstance: { getFilters: () => ({ type: "sorter", value: this.sortFilter, }), }, }, }); } setSortableSet() { this.sortableSet = {}; this.dataFields.forEach((dataField) => { this.sortableSet[dataField.id] = dataField.sortable ?? true; }); } flushTableData() { this.idle = true; this.tableData = []; } scrollTypeChanged(configurationChange) { return (!!configurationChange.previousValue && configurationChange.previousValue.scrollType !== configurationChange.currentValue.scrollType); } getTableScrollRange() { // Note: To work properly virtual viewport should be scrollable // to ensure that container will be scrollable we're adding 50% more items // Ex: Viewport height: 100px, itemSize: 20px => 5 (range) in current configuration the viewport // will not be scrollable/work properly because all the items fits the screen. // We're adding 50% more items and in this case, we're covered even the user provides 0% buffer. const internalBuffer = 2; const scrollBuffer = (this._scrollBuffer ?? 0) / 100 + 1; if (this._range) { // Note: In case user provided a range we don't need // to add our internal buffer. Totally relying on user calculus return Math.floor(this._range * scrollBuffer); } return Math.floor((this.tableWidgetHeight / this.rowHeight) * internalBuffer * scrollBuffer); } onPagerAction(page) { this.eventBus .getStream(types_1.SET_NEXT_PAGE) .next({ payload: page, id: this.componentId }); } initPrefetchAddon() { if (this.hasVirtualScroll) { this.virtualScrollAddon.initVirtualScroll(this); } else if (this.hasPaginator) { this.paginatorAddon.initPaginator(this); } else { this.virtualScrollAddon.initVirtualScroll(this); } } resolveSortBy(changes) { // Note: We don't have to trigger sorting in case sortable flag is false. // Note: Using true as a default sortable value to maintain backward compatibility if (this.isSortByUpdated(changes.configuration) && (changes.configuration.currentValue.sortable ?? true)) { const sortedColumn = { direction: changes.configuration.currentValue .sorterConfiguration.descendantSorting ? bits_1.SorterDirection.descending : bits_1.SorterDirection.ascending, sortBy: changes.configuration.currentValue.sorterConfiguration .sortBy, }; this.onSortOrderChanged(sortedColumn); } } resolveSearch(changes) { if (!(0, isEqual_1.default)(changes.configuration.currentValue ?.searchConfiguration, changes.configuration.previousValue ?.searchConfiguration)) { this.searchAddon.initWidget(this); } } }; exports.TableWidgetComponent = TableWidgetComponent; tslib_1.__decorate([ (0, core_1.Input)(), tslib_1.__metadata("design:type", Array) ], TableWidgetComponent.prototype, "widgetData", void 0); tslib_1.__decorate([ (0, core_1.Input)(), tslib_1.__metadata("design:type", String) ], TableWidgetComponent.prototype, "componentId", void 0); tslib_1.__decorate([ (0, core_1.Input)(), tslib_1.__metadata("design:type", Object) ], TableWidgetComponent.prototype, "configuration", void 0); tslib_1.__decorate([ (0, core_1.Input)(), tslib_1.__metadata("design:type", Array) ], TableWidgetComponent.prototype, "dataFields", void 0); tslib_1.__decorate([ (0, core_1.Input)(), tslib_1.__metadata("design:type", Number) ], TableWidgetComponent.prototype, "totalItems", void 0); tslib_1.__decorate([ (0, core_1.Input)(), tslib_1.__metadata("design:type", Number) ], TableWidgetComponent.prototype, "indentFromTop", void 0); tslib_1.__decorate([ (0, core_1.Input)(), tslib_1.__metadata("design:type", Boolean) ], TableWidgetComponent.prototype, "sortable", void 0); tslib_1.__decorate([ (0, core_1.Input)(), tslib_1.__metadata("design:type", Boolean) ], TableWidgetComponent.prototype, "delayedMousePresenceDetectionEnabled", void 0); tslib_1.__decorate([ (0, core_1.Input)(), (0, core_1.HostBinding)("class"), tslib_1.__metadata("design:type", String) ], TableWidgetComponent.prototype, "elementClass", void 0); tslib_1.__decorate([ (0, core_1.ViewChild)("widgetTable"), tslib_1.__metadata("design:type", bits_1.TableComponent) ], TableWidgetComponent.prototype, "table", void 0); tslib_1.__decorate([ (0, core_1.ViewChild)("paginator"), tslib_1.__metadata("design:type", bits_1.PaginatorComponent) ], TableWidgetComponent.prototype, "paginator", void 0); tslib_1.__decorate([ (0, core_1.ViewChild)(scrolling_1.CdkVirtualScrollViewport), tslib_1.__metadata("design:type", scrolling_1.CdkVirtualScrollViewport) ], TableWidgetComponent.prototype, "vscrollViewport", void 0); tslib_1.__decorate([ (0, core_1.ViewChildren)(bits_1.TableRowComponent, { read: core_1.ElementRef }), tslib_1.__metadata("design:type", core_1.QueryList) ], TableWidgetComponent.prototype, "tableRows", void 0); tslib_1.__decorate([ (0, core_1.Input)(), tslib_1.__metadata("design:type", Number), tslib_1.__metadata("design:paramtypes", [Number]) ], TableWidgetComponent.prototype, "range", null); exports.TableWidgetComponent = TableWidgetComponent = tslib_1.__decorate([ (0, core_1.Component)({ selector: "nui-table-widget", templateUrl: "./table-widget.component.html", styleUrls: ["./table-widget.component.less"], changeDetection: core_1.ChangeDetectionStrategy.OnPush, providers: [ search_feature_addon_service_1.SearchFeatureAddonService, virtual_scroll_feature_addon_service_1.VirtualScrollFeatureAddonService, paginator_feature_addon_service_1.PaginatorFeatureAddonService, ], host: { // Note: Moved here from configuration to ensure that consumers will not override it. // Used to prevent table overflowing preview container in the edit/configuration mode. "[class.table-widget-fullwidth]": "true", }, }), tslib_1.__param(0, (0, core_1.Inject)(types_2.PIZZAGNA_EVENT_BUS)), tslib_1.__param(1, (0, core_1.Optional)()), tslib_1.__param(1, (0, core_1.Inject)(types_2.DATA_SOURCE)), tslib_1.__param(2, (0, core_1.Optional)()), tslib_1.__metadata("design:paramtypes", [bits_1.EventBus, Object, widget_configuration_service_1.WidgetConfigurationService, core_1.ChangeDetectorRef, pizzagna_service_1.PizzagnaService, bits_1.VirtualViewportManager, core_1.NgZone, core_1.ElementRef, bits_1.LoggerService, search_feature_addon_service_1.SearchFeatureAddonService, paginator_feature_addon_service_1.PaginatorFeatureAddonService, virtual_scroll_feature_addon_service_1.VirtualScrollFeatureAddonService, table_formatter_registry_service_1.TableFormatterRegistryService, bits_1.SelectorService]) ], TableWidgetComponent); //# sourceMappingURL=table-widget.component.js.map