UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

588 lines (584 loc) 73.6 kB
import { NgTemplateOutlet, DecimalPipe, AsyncPipe } from '@angular/common'; import * as i0 from '@angular/core'; import { Injectable, inject, input, linkedSignal, signal, computed, ChangeDetectionStrategy, Component, viewChild, DestroyRef } from '@angular/core'; import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Router } from '@angular/router'; import { MeasurementService, InventoryService } from '@c8y/client'; import * as i1 from '@c8y/ngx-components'; import { AlertService, DashboardChildComponent, GroupService, DynamicComponentAlertAggregator, DynamicComponentAlert, C8yTranslateDirective, DeviceStatusComponent, DynamicComponentModule, EmptyStateComponent, ForOfDirective, GuideDocsComponent, GuideHrefDirective, IconDirective, ListGroupModule, LoadingComponent, VirtualScrollListenerDirective, WidgetActionWrapperComponent, ApplyRangeClassPipe, C8yTranslatePipe, DatePipe, FormGroupComponent } from '@c8y/ngx-components'; import { DatapointsExportSelectorComponent } from '@c8y/ngx-components/datapoints-export-selector'; import { gettext } from '@c8y/ngx-components/gettext'; import { REFRESH_OPTION, TIME_INTERVAL, GLOBAL_CONTEXT_DISPLAY_MODE, WidgetConfigMigrationService, CONTEXT_FEATURE, PRESET_NAME, GlobalContextConnectorComponent, LocalControlsComponent } from '@c8y/ngx-components/global-context'; import { isEqual, merge } from 'lodash-es'; import { pairwise, filter, debounceTime, take, distinctUntilChanged } from 'rxjs'; import * as i1$1 from '@angular/cdk/drag-drop'; import { DragDropModule } from '@angular/cdk/drag-drop'; import * as i3 from '@angular/forms'; import { NgForm, FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { WidgetConfigService } from '@c8y/ngx-components/context-dashboard'; import * as i4 from 'ngx-bootstrap/popover'; import { PopoverModule } from 'ngx-bootstrap/popover'; /** * Default column configuration for datapoints list widget */ const DEFAULT_DATAPOINTS_LIST_COLUMNS = [ { id: 'kpi', label: gettext('Label'), visible: true, order: 0 }, { id: 'target', label: gettext('Target'), visible: true, order: 1 }, { id: 'current', label: gettext('Current'), visible: true, order: 2 }, { id: 'diff', label: gettext('Diff'), visible: true, order: 3 }, { id: 'diffPercentage', label: gettext('Diff %'), visible: true, order: 4 }, { id: 'asset', label: gettext('Asset'), visible: true, order: 5 } ]; /** * Defaults for legacy AngularJS datapoints list widgets that arrive without any * time-context fields. Mirrors the legacy "show all data" behavior. Factory so * `dateTo` is read at call time. */ function getLegacyDatapointsListDefaults() { return { displayMode: GLOBAL_CONTEXT_DISPLAY_MODE.CONFIG, dateTimeContext: { dateFrom: '1970-01-01T00:00:00.000Z', dateTo: new Date().toISOString(), interval: TIME_INTERVAL.CUSTOM }, isAutoRefreshEnabled: true, refreshOption: REFRESH_OPTION.LIVE }; } class DatapointsListService { /** * Calculate difference between current value and target * @param datapoint - Datapoint record * @returns Difference value or null if value/target is undefined */ diff(datapoint) { const { currentValue, target } = datapoint; // != checks both null and undefined if (currentValue != null && target != null) { return currentValue - target; } return null; } /** * Calculate percentage difference between current value and target * @param datapoint - Datapoint record * @returns Percentage difference or null if target is undefined */ diffPercent(datapoint) { const target = datapoint.target; if (target !== null && target !== undefined) { const _diff = this.diff(datapoint); if (_diff !== null) { // Intentionally allows division by zero to return Infinity // when target is 0, representing mathematically undefined percentage. // This follows the previous AngularJS implementation behavior. return (_diff / target) * 100; } } return null; } /** * Get fraction size format based on whether the value is an integer * @param value - Number to check * @param defaultFractionSize - Default fraction size format to use for non-integers * @returns Fraction size format ('1.0-0' for integers, defaultFractionSize for decimals) */ getFractionSize(value, defaultFractionSize) { if (value === null || value === undefined) { return defaultFractionSize; } return value % 1 === 0 ? '1.0-0' : defaultFractionSize; } /** * Extract current value and timestamp from a measurement * @param datapoint - Datapoint configuration (contains fragment and series) * @param measurement - Measurement to extract value from * @returns Object containing extracted value and timestamp (null if not found) */ extractMeasurementValue(datapoint, measurement) { // Return null values if measurement is missing if (!measurement) { return { value: null, timestamp: null }; } // Return null values if datapoint configuration is incomplete if (!datapoint.fragment || !datapoint.series) { return { value: null, timestamp: null }; } // Access the fragment (e.g., "c8y_Steam") const fragmentData = measurement[datapoint.fragment]; if (!fragmentData) { return { value: null, timestamp: null }; } // Access the series within the fragment (e.g., "Temperature") const seriesData = fragmentData[datapoint.series]; if (!seriesData || seriesData.value === undefined) { return { value: null, timestamp: null }; } // Extract and return the value and timestamp return { value: seriesData.value, timestamp: new Date(measurement.time) }; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DatapointsListService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DatapointsListService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DatapointsListService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * Service that handles all data-fetching operations for the datapoints list widget. * Encapsulates measurement fetching, data enrichment, and error handling. */ class DatapointsListFetchService { constructor() { this.alertService = inject(AlertService); this.measurementService = inject(MeasurementService); this.inventoryService = inject(InventoryService); this.datapointsListService = inject(DatapointsListService); } /** * Fetch measurements for all active datapoints and enrich them with calculated values. */ async fetchDatapointsWithMeasurements(datapoints, config) { const targetManagedObjects = new Map(); const results = await this.fetchAllDatapoints(datapoints, config); const enrichedDataPoints = results.map(r => r.datapoint); const seriesWithoutPermissionCount = results.filter(r => r.hasPermissionError).length; await this.fetchTargetManagedObjects(enrichedDataPoints, targetManagedObjects); return { dataPoints: enrichedDataPoints, seriesWithoutPermissionCount, targetManagedObjects }; } /** * Fetch and enrich all datapoints in parallel. * Each datapoint fetch is independent - one failure doesn't affect others. */ fetchAllDatapoints(datapoints, config) { return Promise.all(datapoints.map((datapoint, index) => this.fetchSingleDatapoint(datapoint, index, config))); } /** * Fetch measurement for a single datapoint and return enriched result. * Handles errors gracefully - returns empty datapoint on failure. */ async fetchSingleDatapoint(datapoint, index, config) { try { const measurement = await this.getMeasurementForDatapoint(datapoint, config); const measurementValue = this.datapointsListService.extractMeasurementValue(datapoint, measurement); return { datapoint: this.createEnrichedDatapoint(datapoint, index, measurementValue, config), hasPermissionError: false }; } catch (error) { return this.handleFetchError(error, datapoint, index, config); } } /** * Handle fetch error and return appropriate result. */ handleFetchError(error, datapoint, index, config) { const isPermissionError = error?.status === 403; if (!isPermissionError) { this.alertService.addServerFailure(error); } return { datapoint: this.createEnrichedDatapoint(datapoint, index, { value: null, timestamp: null }, config), hasPermissionError: isPermissionError }; } /** * Create an enriched datapoint with measurement value and all calculated fields. */ createEnrichedDatapoint(datapoint, index, measurementValue, config) { const enrichedDatapoint = { ...datapoint, id: this.getDatapointId(datapoint, index), currentValue: measurementValue.value, timestamp: measurementValue.timestamp }; this.calculateDerivedFields(enrichedDatapoint, config.fractionSize); return enrichedDatapoint; } /** * Calculate and set all derived fields on a datapoint. * Includes diff, diffPercent, and fraction sizes for display formatting. */ calculateDerivedFields(datapoint, fractionSize) { datapoint.diffValue = this.datapointsListService.diff(datapoint); datapoint.diffPercentValue = this.datapointsListService.diffPercent(datapoint); datapoint.currentFractionSize = this.datapointsListService.getFractionSize(datapoint.currentValue, fractionSize); datapoint.diffFractionSize = this.datapointsListService.getFractionSize(datapoint.diffValue, fractionSize); datapoint.diffPercentFractionSize = this.datapointsListService.getFractionSize(datapoint.diffPercentValue, fractionSize); } /** * Fetch managed objects for device status display. */ async fetchTargetManagedObjects(dataPoints, targetManagedObjects) { const uniqueTargetIds = this.getUniqueTargetIds(dataPoints); if (uniqueTargetIds.size === 0) { return; } await Promise.all(Array.from(uniqueTargetIds).map(targetId => this.fetchManagedObject(targetId, targetManagedObjects))); } /** * Get unique target IDs from datapoints. */ getUniqueTargetIds(dataPoints) { const uniqueTargetIds = new Set(); for (const dp of dataPoints) { if (dp.__target?.id) { uniqueTargetIds.add(dp.__target.id.toString()); } } return uniqueTargetIds; } /** * Fetch a single managed object and add to the map. */ async fetchManagedObject(targetId, targetManagedObjects) { try { const { data } = await this.inventoryService.detail(targetId); targetManagedObjects.set(targetId, data); } catch (error) { this.alertService.addServerFailure(error); } } /** * Fetch the most recent measurement for a datapoint within the specified date range. */ async getMeasurementForDatapoint(datapoint, config) { const sourceId = datapoint.__target?.id; if (!sourceId || !datapoint.fragment || !datapoint.series) { return null; } const filter = { dateFrom: config.dateFrom || '1970-01-01', dateTo: config.dateTo || new Date().toISOString(), source: sourceId, valueFragmentSeries: datapoint.series, valueFragmentType: datapoint.fragment, pageSize: 1, revert: true }; const { data } = await this.measurementService.list(filter); return data?.[0] ?? null; } /** * Get unique ID for a datapoint. * Uses target ID if available, otherwise generates fallback based on index. */ getDatapointId(datapoint, fallbackIndex) { return datapoint.__target?.id?.toString() || `dp-${fallbackIndex}`; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DatapointsListFetchService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DatapointsListFetchService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DatapointsListFetchService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class DatapointsListViewComponent { constructor() { this.config = input.required(...(ngDevMode ? [{ debugName: "config" }] : [])); this.isInPreviewMode = input(false, ...(ngDevMode ? [{ debugName: "isInPreviewMode" }] : [])); this.alertService = inject(AlertService); this.dashboardChild = inject(DashboardChildComponent, { optional: true }); this.defaultColumns = DEFAULT_DATAPOINTS_LIST_COLUMNS; this.fetchService = inject(DatapointsListFetchService); this.groupService = inject(GroupService); this.inventoryService = inject(InventoryService); this.router = inject(Router); this.widgetConfigMigrationService = inject(WidgetConfigMigrationService); this.CONTEXT_FEATURE = CONTEXT_FEATURE; this.GLOBAL_CONTEXT_DISPLAY_MODE = GLOBAL_CONTEXT_DISPLAY_MODE; this.missingAllPermissionsAlert = new DynamicComponentAlertAggregator(); this.targetManagedObjects = new Map(); this.configSignal = linkedSignal(() => this.config(), ...(ngDevMode ? [{ debugName: "configSignal" }] : [])); this.contextConfig = signal({}, ...(ngDevMode ? [{ debugName: "contextConfig" }] : [])); this.dataPoints = signal([], ...(ngDevMode ? [{ debugName: "dataPoints" }] : [])); this.displayMode = signal(GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD, ...(ngDevMode ? [{ debugName: "displayMode" }] : [])); this.hasLoadedOnce = signal(false, ...(ngDevMode ? [{ debugName: "hasLoadedOnce" }] : [])); this.hasNoPermissionsToReadAnyMeasurement = signal(false, ...(ngDevMode ? [{ debugName: "hasNoPermissionsToReadAnyMeasurement" }] : [])); this.isLinkedToGlobal = signal(undefined, ...(ngDevMode ? [{ debugName: "isLinkedToGlobal" }] : [])); this.isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : [])); this.widgetControls = signal(PRESET_NAME.DATA_POINTS_LIST, ...(ngDevMode ? [{ debugName: "widgetControls" }] : [])); this.fractionSize = computed(() => { const decimalPlaces = this.configSignal()?.decimalPlaces; if (typeof decimalPlaces === 'number' && !Number.isNaN(decimalPlaces)) { return `1.${decimalPlaces}-${decimalPlaces}`; } return '1.2-2'; }, ...(ngDevMode ? [{ debugName: "fractionSize" }] : [])); this.columns = computed(() => { const options = this.configSignal()?.options; if (options?.columns?.length > 0) { return [...options.columns].sort((a, b) => a.order - b.order); } return this.defaultColumns.map(defaultCol => ({ ...defaultCol, visible: options?.[defaultCol.id] ?? defaultCol.visible })); }, ...(ngDevMode ? [{ debugName: "columns" }] : [])); this.visibleColumns = computed(() => { return this.columns().filter(col => col.visible !== false); }, ...(ngDevMode ? [{ debugName: "visibleColumns" }] : [])); this.exportConfig = computed(() => { const effectiveConfig = { ...this.configSignal(), ...this.contextConfig() }; const dateFrom = effectiveConfig.dateTimeContext?.dateFrom; const dateTo = effectiveConfig.dateTimeContext?.dateTo; if (!dateFrom || !dateTo || (this.isLoading() && !this.hasLoadedOnce())) { return null; } return { exportType: 'latestWithDetails', datapointDetails: effectiveConfig.datapoints ?.filter(dp => dp.__active === true) .map(dp => ({ deviceName: dp.__target?.name || '', source: dp.__target?.id || '', valueFragmentSeries: dp.series, valueFragmentType: dp.fragment, target: dp.target, label: dp.label })) || [], dateFrom: dateFrom instanceof Date ? dateFrom.toISOString() : dateFrom, dateTo: dateTo instanceof Date ? dateTo.toISOString() : dateTo, columns: this.columns() .filter(col => col.visible) .map(col => ({ id: col.id, label: col.label, visible: col.visible, order: col.order })) }; }, ...(ngDevMode ? [{ debugName: "exportConfig" }] : [])); this.activeDataPoints = computed(() => { return this.configSignal()?.datapoints?.filter(dp => dp.__active === true) ?? []; }, ...(ngDevMode ? [{ debugName: "activeDataPoints" }] : [])); this.loadRequestId = 0; this.seriesWithoutPermissionToReadCount = signal(0, ...(ngDevMode ? [{ debugName: "seriesWithoutPermissionToReadCount" }] : [])); toObservable(this.config) .pipe(pairwise(), takeUntilDestroyed()) .subscribe(([prevConfig, currentConfig]) => { if (!this.isInPreviewMode()) { return; } const prevContext = this.extractContextState(prevConfig); const newContext = this.extractContextState(currentConfig); this.contextConfig.set(newContext); const datapointsChanged = !isEqual(prevConfig.datapoints, currentConfig.datapoints); const decimalPlacesChanged = prevConfig.decimalPlaces !== currentConfig.decimalPlaces; const contextChanged = !isEqual(prevContext, newContext); if (datapointsChanged || decimalPlacesChanged || contextChanged) { this.isLoading.set(true); this.loadDatapoints(); } }); } ngOnInit() { this.applyConfigMigration(); const config = this.configSignal(); const displayMode = config.displayMode || GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD; this.displayMode.set(displayMode); this.contextConfig.set(this.extractContextState(config)); // First fetch is triggered by the global-context connector's initial configChange, // except in preview mode where the connector is not rendered (see template). if (this.isInPreviewMode()) { this.loadDatapoints(); } } onContextChange(event) { const { diff, context } = event; this.contextConfig.set(context); if (diff.isAutoRefreshEnabled === false && Object.keys(diff).length === 1 && context.refreshOption === REFRESH_OPTION.LIVE) { return; } this.isLoading.set(true); this.loadDatapoints(); } onRefresh() { this.isLoading.set(true); this.loadDatapoints(); } onExportModalOpen(isOpened) { this.setAutoRefreshPaused(isOpened); } async redirectToAsset(assetId) { if (this.isInPreviewMode() || !assetId) { return; } try { const { data: mo } = await this.inventoryService.detail(assetId); if (mo) { const assetPath = this.groupService.getAssetPath(mo); this.router.navigateByUrl(`/${assetPath}/${mo.id}`); } } catch (error) { this.alertService.addServerFailure(error); } } getTargetManagedObject(targetId) { return this.targetManagedObjects.get(targetId.toString()); } getDashboardChild() { return this.dashboardChild ?? null; } getRangeValues(dp) { return { yellowRangeMin: dp.yellowRangeMin, yellowRangeMax: dp.yellowRangeMax, redRangeMin: dp.redRangeMin, redRangeMax: dp.redRangeMax }; } onListScrolled() { this.setAutoRefreshPaused(true); } onListScrolledToTop() { this.setAutoRefreshPaused(false); } /** Runs once on init; subsequent input changes are already in the new format. */ applyConfigMigration() { const raw = this.config(); const options = this.widgetConfigMigrationService.hasNoTimeContextFields(raw) ? { legacyTimeContextDefaults: getLegacyDatapointsListDefaults() } : undefined; const migrated = this.widgetConfigMigrationService.migrateWidgetConfig(raw, options); if (migrated !== raw) { this.configSignal.set(merge({}, raw, migrated)); } } extractContextState(config) { return { dateTimeContext: config.dateTimeContext, aggregation: config.aggregation, isAutoRefreshEnabled: config.isAutoRefreshEnabled, refreshInterval: config.refreshInterval, refreshOption: config.refreshOption }; } setAutoRefreshPaused(paused) { if (this.isInPreviewMode()) { return; } const current = this.contextConfig(); if (current.refreshOption === REFRESH_OPTION.HISTORY) { return; } this.contextConfig.set({ ...current, isAutoRefreshEnabled: !paused }); this.isLinkedToGlobal.set(!paused); } loadDatapoints() { if (this.activeDataPoints().length > 0) { this.fetchMeasurements(); } else { this.dataPoints.set([]); this.hasLoadedOnce.set(true); this.isLoading.set(false); } } async fetchMeasurements() { const requestId = ++this.loadRequestId; try { this.isLoading.set(true); const effectiveConfig = { ...this.configSignal(), ...this.contextConfig() }; const dateFrom = effectiveConfig.dateTimeContext?.dateFrom; const dateTo = effectiveConfig.dateTimeContext?.dateTo; const dateFromStr = dateFrom instanceof Date ? dateFrom.toISOString() : dateFrom; const dateToStr = dateTo instanceof Date ? dateTo.toISOString() : dateTo; const result = await this.fetchService.fetchDatapointsWithMeasurements(this.activeDataPoints(), { fractionSize: this.fractionSize(), dateFrom: dateFromStr, dateTo: dateToStr }); if (requestId === this.loadRequestId) { this.dataPoints.set(result.dataPoints); this.targetManagedObjects = result.targetManagedObjects; this.hasLoadedOnce.set(true); this.seriesWithoutPermissionToReadCount.set(result.seriesWithoutPermissionCount); this.checkAndDisplayPermissionErrors(); } } finally { if (requestId === this.loadRequestId) { this.isLoading.set(false); } } } checkAndDisplayPermissionErrors() { if (this.seriesWithoutPermissionToReadCount()) { this.missingAllPermissionsAlert.clear(); this.handleNoPermissionErrorMessage(); } } handleNoPermissionErrorMessage() { const noPermissions = this.seriesWithoutPermissionToReadCount() === this.activeDataPoints().length; this.hasNoPermissionsToReadAnyMeasurement.set(noPermissions); if (noPermissions) { this.showMessageForMissingPermissionsForAllSeries(); } } showMessageForMissingPermissionsForAllSeries() { this.missingAllPermissionsAlert.addAlerts(new DynamicComponentAlert({ allowHtml: true, text: gettext(`<p>To view data, you must meet at least one of these criteria:</p> <ul> <li> Have <b>READ permission for "Measurements" permission type</b> (either as a global role or for the specific source) </li> <li> Be the <b>owner of the source</b> you want to export data from </li> </ul> <p>Don't meet these requirements? Contact your system administrator for assistance.</p>`), type: 'system' })); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DatapointsListViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: DatapointsListViewComponent, isStandalone: true, selector: "c8y-datapoints-list", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, isInPreviewMode: { classPropertyName: "isInPreviewMode", publicName: "isInPreviewMode", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "d-col fit-h" }, ngImport: i0, template: "@if (!isInPreviewMode()) {\n <div class=\"d-flex gap-16 p-r-16 inner-scroll h-auto min-width-0\">\n @if (displayMode() === GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD) {\n <c8y-global-context-connector\n [controls]=\"widgetControls()\"\n [config]=\"contextConfig()\"\n [isLoading]=\"isLoading()\"\n [dashboardChild]=\"getDashboardChild()!\"\n [linked]=\"isLinkedToGlobal()\"\n (configChange)=\"onContextChange($event)\"\n (refresh)=\"onRefresh()\"\n ></c8y-global-context-connector>\n } @else {\n <c8y-local-controls\n [controls]=\"widgetControls()\"\n [displayMode]=\"displayMode()\"\n [config]=\"contextConfig()\"\n [isLoading]=\"isLoading()\"\n (configChange)=\"onContextChange($event)\"\n (refresh)=\"onRefresh()\"\n ></c8y-local-controls>\n }\n </div>\n}\n\n@if (!isInPreviewMode() && exportConfig(); as config) {\n <c8y-widget-action>\n <c8y-datapoints-export-selector\n [displayMode]=\"'icon-only'\"\n [exportConfig]=\"config\"\n [containerClass]=\"'d-contents'\"\n (isOpen)=\"onExportModalOpen($event)\"\n ></c8y-datapoints-export-selector>\n </c8y-widget-action>\n}\n\n@if (!hasNoPermissionsToReadAnyMeasurement()) {\n <!-- the .page-sticky-header -->\n @if (dataPoints().length > 0) {\n <div class=\"hidden-xs hidden-sm c8y-list__item\">\n <div class=\"c8y-list__item__block flex-grow min-width-0\">\n <div class=\"c8y-list__item__icon\">\n <i style=\"width: 22px\"></i>\n </div>\n <div class=\"c8y-list__item__body\">\n <div class=\"d-flex-md row\">\n @for (column of visibleColumns(); track column.id) {\n <div\n [class]=\"\n column.id === 'kpi' || column.id === 'asset'\n ? 'col-md-3 flex-grow min-width-0'\n : column.id == 'diff' || column.id == 'diffPercentage'\n ? 'col-md-1 flex-grow min-width-0'\n : 'col-md-2 flex-grow min-width-0'\n \"\n [class.text-right]=\"column.id !== 'asset' && column.id !== 'kpi'\"\n >\n <span class=\"text-medium text-truncate\">\n {{ column.label | translate }}\n </span>\n </div>\n }\n </div>\n </div>\n </div>\n </div>\n }\n <!-- The record list -->\n @if (isLoading() && !hasLoadedOnce()) {\n <!-- Initial load: full spinner -->\n <ng-container [ngTemplateOutlet]=\"loading\"></ng-container>\n } @else {\n @if (isLoading()) {\n <!-- Refresh: inline loading overlay -->\n <div class=\"p-absolute fit-w overflow-hidden p-b-4\">\n <c8y-loading [layout]=\"'page'\"></c8y-loading>\n </div>\n }\n @if (dataPoints().length) {\n <c8y-list-group\n class=\"flex-grow\"\n role=\"list\"\n c8yVirtualScrollListener\n (scrolled)=\"onListScrolled()\"\n (scrolledToTop)=\"onListScrolledToTop()\"\n >\n <c8y-li\n role=\"listitem\"\n *c8yFor=\"\n let dp of { data: dataPoints(), res: null! };\n enableVirtualScroll: true;\n virtualScrollElementSize: 48;\n virtualScrollStrategy: 'fixed'\n \"\n >\n <c8y-li-icon>\n <i\n c8yIcon=\"circle\"\n [style.color]=\"dp.color\"\n ></i>\n </c8y-li-icon>\n <c8y-li-body>\n <div class=\"d-flex-md row\">\n @for (column of visibleColumns(); track column.id) {\n @switch (column.id) {\n @case ('kpi') {\n <div\n class=\"col-md-3 flex-grow\"\n [attr.data-cy]=\"'datapointlist-' + column.id\"\n >\n <div class=\"d-flex a-i-center d-contents-md p-t-4 p-b-4 separator-bottom\">\n <small\n class=\"text-label-small flex-grow visible-xs-inline-block visible-sm-inline-block\"\n >{{ column.label | translate }}</small\n >\n {{ dp.label | translate }}\n @if (dp.unit) {\n <small class=\"text-muted\">{{ dp.unit }}</small>\n }\n </div>\n </div>\n }\n @case ('target') {\n <div\n class=\"col-md-2 text-right-md flex-grow\"\n [attr.data-cy]=\"'datapointlist-' + column.id\"\n >\n <div class=\"d-flex a-i-center d-contents-md p-t-4 p-b-4 separator-bottom\">\n <small\n class=\"text-label-small flex-grow visible-xs-inline-block visible-sm-inline-block\"\n >{{ column.label | translate }}</small\n >\n <span>{{ dp.target }}</span>\n </div>\n </div>\n }\n @case ('current') {\n <div\n class=\"col-md-2 text-right-md flex-grow\"\n [attr.data-cy]=\"'datapointlist-' + column.id\"\n >\n <div class=\"d-flex a-i-center d-contents-md p-t-4 p-b-4 separator-bottom\">\n <small\n class=\"text-label-small flex-grow visible-xs-inline-block visible-sm-inline-block\"\n >{{ column.label | translate }}</small\n >\n @let ariaLabel =\n 'Last updated: {{ date }}'\n | translate: { date: dp.timestamp | c8yDate: 'medium' };\n <span\n [class]=\"dp.currentValue | applyRangeClass: getRangeValues(dp)\"\n [title]=\"dp.timestamp | c8yDate: 'medium'\"\n [attr.aria-label]=\"ariaLabel\"\n >\n {{ dp.currentValue | number: dp.currentFractionSize }}\n </span>\n </div>\n </div>\n }\n @case ('diff') {\n <div\n class=\"col-md-1 text-right-md flex-grow\"\n [attr.data-cy]=\"'datapointlist-' + column.id\"\n >\n <div class=\"d-flex a-i-center d-contents-md p-t-4 p-b-4 separator-bottom\">\n <small\n class=\"text-label-small flex-grow visible-xs-inline-block visible-sm-inline-block\"\n >{{ column.label | translate }}</small\n >\n <span>\n {{ dp.diffValue | number: dp.diffFractionSize }}\n </span>\n </div>\n </div>\n }\n @case ('diffPercentage') {\n <div\n class=\"col-md-1 text-right-md flex-grow\"\n [attr.data-cy]=\"'datapointlist-' + column.id\"\n >\n <div class=\"d-flex a-i-center d-contents-md p-t-4 p-b-4 separator-bottom\">\n <small\n class=\"text-label-small flex-grow visible-xs-inline-block visible-sm-inline-block\"\n >{{ column.label | translate }}</small\n >\n <span>\n {{ dp.diffPercentValue | number: dp.diffPercentFractionSize }}\n </span>\n </div>\n </div>\n }\n @case ('asset') {\n <div\n class=\"col-md-3 flex-grow\"\n [attr.data-cy]=\"'datapointlist-' + column.id\"\n >\n <div class=\"d-flex a-i-center d-contents-md p-t-4 p-b-4 separator-bottom\">\n <small\n class=\"text-label-small flex-grow visible-xs-inline-block visible-sm-inline-block\"\n >{{ column.label | translate }}</small\n >\n @let ariaLabel =\n 'Navigate to asset: \"{{ name }}\"'\n | translate: { name: dp.__target?.name };\n <button\n class=\"btn-clean d-flex a-i-center gap-4 text-muted\"\n [attr.aria-label]=\"ariaLabel\"\n type=\"button\"\n (click)=\"redirectToAsset(dp.__target?.id)\"\n >\n @if (dp.__target?.id && getTargetManagedObject(dp.__target.id); as mo) {\n <c8y-device-status\n [mo]=\"mo\"\n [size]=\"16\"\n ></c8y-device-status>\n }\n <small\n class=\"text-truncate\"\n [title]=\"dp.__target?.name\"\n >{{ dp.__target?.name }}</small\n >\n </button>\n </div>\n </div>\n }\n }\n }\n </div>\n </c8y-li-body>\n </c8y-li>\n </c8y-list-group>\n } @else {\n <ng-container [ngTemplateOutlet]=\"emptyState\"></ng-container>\n }\n }\n} @else {\n <div class=\"p-t-24 p-r-16 p-l-16 p-b-16 d-flex\">\n <div class=\"center-block\">\n <c8y-dynamic-component-alerts\n [alerts]=\"missingAllPermissionsAlert\"\n ></c8y-dynamic-component-alerts>\n </div>\n </div>\n}\n\n<ng-template #loading>\n <c8y-loading></c8y-loading>\n</ng-template>\n\n<ng-template #emptyState>\n <div class=\"p-relative p-l-24\">\n <c8y-ui-empty-state\n [icon]=\"'c8y-alert-idle'\"\n [title]=\"'No data to display.' | translate\"\n [horizontal]=\"true\"\n data-cy=\"datapoints-list--empty-state\"\n >\n <p c8y-guide-docs>\n <small\n translate\n ngNonBindable\n >\n Find out more in the\n <a c8y-guide-href=\"/docs/cockpit/widgets-collection/#data-point-list\"\n >user documentation</a\n >.\n </small>\n </p>\n </c8y-ui-empty-state>\n </div>\n</ng-template>\n", dependencies: [{ kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: DatapointsExportSelectorComponent, selector: "c8y-datapoints-export-selector", inputs: ["displayMode", "containerClass", "exportConfig"], outputs: ["isOpen"] }, { kind: "component", type: DeviceStatusComponent, selector: "device-status, c8y-device-status", inputs: ["mo", "size"] }, { kind: "ngmodule", type: DynamicComponentModule }, { kind: "component", type: i1.DynamicComponentAlertsComponent, selector: "c8y-dynamic-component-alerts", inputs: ["alerts"] }, { kind: "component", type: EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "directive", type: ForOfDirective, selector: "[c8yFor]", inputs: ["c8yForOf", "c8yForLoadMore", "c8yForPipe", "c8yForNotFound", "c8yForMaxIterations", "c8yForLoadingTemplate", "c8yForLoadNextLabel", "c8yForLoadingLabel", "c8yForRealtime", "c8yForRealtimeOptions", "c8yForComparator", "c8yForEnableVirtualScroll", "c8yForVirtualScrollElementSize", "c8yForVirtualScrollStrategy", "c8yForVirtualScrollContainerHeight"], outputs: ["c8yForCount", "c8yForChange", "c8yForLoadMoreComponent"] }, { kind: "component", type: GlobalContextConnectorComponent, selector: "c8y-global-context-connector", inputs: ["controls", "config", "isLoading", "dashboardChild", "linked", "emitRefresh"], outputs: ["configChange", "refresh", "linkedChange"] }, { kind: "component", type: GuideDocsComponent, selector: "[c8y-guide-docs]" }, { kind: "directive", type: GuideHrefDirective, selector: "[c8y-guide-href]", inputs: ["c8y-guide-href"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "ngmodule", type: ListGroupModule }, { kind: "component", type: i1.ListGroupComponent, selector: "c8y-list-group" }, { kind: "component", type: i1.ListItemComponent, selector: "c8y-list-item, c8y-li", inputs: ["active", "highlighted", "emptyActions", "dense", "collapsed", "selectable"], outputs: ["collapsedChange"] }, { kind: "component", type: i1.ListItemIconComponent, selector: "c8y-list-item-icon, c8y-li-icon", inputs: ["icon", "status"] }, { kind: "component", type: i1.ListItemBodyComponent, selector: "c8y-list-item-body, c8y-li-body", inputs: ["body"] }, { kind: "component", type: LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "message"] }, { kind: "component", type: LocalControlsComponent, selector: "c8y-local-controls", inputs: ["controls", "displayMode", "config", "isLoading", "disabled", "emitRefresh"], outputs: ["configChange", "refresh"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: VirtualScrollListenerDirective, selector: "[c8yVirtualScrollListener]", inputs: ["scrollThreshold"], outputs: ["scrolled", "scrolledToTop"] }, { kind: "component", type: WidgetActionWrapperComponent, selector: "c8y-widget-action" }, { kind: "pipe", type: ApplyRangeClassPipe, name: "applyRangeClass" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: DatePipe, name: "c8yDate" }, { kind: "pipe", type: DecimalPipe, name: "number" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DatapointsListViewComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-datapoints-list', host: { class: 'd-col fit-h' }, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ ApplyRangeClassPipe, C8yTranslateDirective, C8yTranslatePipe, DatapointsExportSelectorComponent, DatePipe, DecimalPipe, DeviceStatusComponent, DynamicComponentModule, EmptyStateComponent, ForOfDirective, GlobalContextConnectorComponent, GuideDocsComponent, GuideHrefDirective, IconDirective, ListGroupModule, LoadingComponent, LocalControlsComponent, NgTemplateOutlet, VirtualScrollListenerDirective, WidgetActionWrapperComponent ], template: "@if (!isInPreviewMode()) {\n <div class=\"d-flex gap-16 p-r-16 inner-scroll h-auto min-width-0\">\n @if (displayMode() === GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD) {\n <c8y-global-context-connector\n [controls]=\"widgetControls()\"\n [config]=\"contextConfig()\"\n [isLoading]=\"isLoading()\"\n [dashboardChild]=\"getDashboardChild()!\"\n [linked]=\"isLinkedToGlobal()\"\n (configChange)=\"onContextChange($event)\"\n (refresh)=\"onRefresh()\"\n ></c8y-global-context-connector>\n } @else {\n <c8y-local-controls\n [controls]=\"widgetControls()\"\n [displayMode]=\"displayMode()\"\n [config]=\"contextConfig()\"\n [isLoading]=\"isLoading()\"\n (configChange)=\"onContextChange($event)\"\n (refresh)=\"onRefresh()\"\n ></c8y-local-controls>\n }\n </div>\n}\n\n@if (!isInPreviewMode() && exportConfig(); as config) {\n <c8y-widget-action>\n <c8y-datapoints-export-selector\n [displayMode]=\"'icon-only'\"\n [exportConfig]=\"config\"\n [containerClass]=\"'d-contents'\"\n (isOpen)=\"onExportModalOpen($event)\"\n ></c8y-datapoints-export-selector>\n </c8y-widget-action>\n}\n\n@if (!hasNoPermissionsToReadAnyMeasurement()) {\n <!-- the .page-sticky-header -->\n @if (dataPoints().length > 0) {\n <div class=\"hidden-xs hidden-sm c8y-list__item\">\n <div class=\"c8y-list__item__block flex-grow min-width-0\">\n <div class=\"c8y-list__item__icon\">\n <i style=\"width: 22px\"></i>\n </div>\n <div class=\"c8y-list__item__body\">\n <div class=\"d-flex-md row\">\n @for (column of visibleColumns(); track column.id) {\n <div\n [class]=\"\n column.id === 'kpi' || column.id === 'asset'\n ? 'col-md-3 flex-grow min-width-0'\n : column.id == 'diff' || column.id == 'diffPercentage'\n ? 'col-md-1 flex-grow min-width-0'\n : 'col-md-2 flex-grow min-width-0'\n \"\n [class.text-right]=\"column.id !== 'asset' && column.id !== 'kpi'\"\n >\n <span class=\"text-medium text-truncate\">\n {{ column.label | translate }}\n </span>\n </div>\n }\n </div>\n </div>\n </div>\n </div>\n }\n <!-- The record list -->\n @if (isLoading() && !hasLoadedOnce()) {\n <!-- Initial load: full spinner -->\n <ng-container [ngTemplateOutlet]=\"loading\"></ng-container>\n } @else {\n @if (isLoading()) {\n <!-- Refresh: inline loading overlay -->\n <div class=\"p-absolute fit-w overflow-hidden p-b-4\">\n <c8y-loading [layout]=\"'page'\"></c8y-loading>\n </div>\n }\n @if (dataPoints().length) {\n <c8y-list-group\n class=\"flex-grow\"\n role=\"list\"\n c8yVirtualScrollListener\n (scrolled)=\"onListScrolled()\"\n (scrolledToTop)=\"onListScrolledToTop()\"\n >\n <c8y-li\n role=\"listitem\"\n *c8yFor=\"\n let dp of { data: dataPoints(), res: null! };\n enableVirtualScroll: true;\n virtualScrollElementSize: 48;\n virtualScrollStrategy: 'fixed'\n \"\n >\n <c8y-li-icon>\n <i\n c8yIcon=\"circle\"\n [style.color]=\"dp.color\"\n ></i>\n </c8y-li-icon>\n <c8y-li-body>\n <div class=\"d-flex-md row\">\n @for (column of visibleColumns(); track column.id) {\n @switch (column.id) {\n @case ('kpi') {\n <div\n class=\"col-md-3 flex-grow\"\n [attr.data-cy]=\"'datapointlist-' + column.id\"\n >\n <div class=\"d-flex a-i-center d-contents-md p-t-4 p-b-4 separator-bottom\">\n <small\n class=\"text-label-small flex-grow visible-xs-inline-block visible-sm-inline-block\"\n >{{ column.label | translate }}</small\n >\n {{ dp.label | translate }}\n @if (dp.unit) {\n <small class=\"text-muted\">{{ dp.unit }}</small>\n }\n </div>\n </div>\n }\n @case ('target') {\n <div\n class=\"col-md-2 text-right-md flex-grow\"\n [attr.data-cy]=\"'datapointlist-' + column.id\"\n >\n <div class=\"d-flex a-i-center d-contents-md p-t-4 p-b-4 separator-bottom\">\n <small\n class=\"text-label-small flex-grow visible-xs-inline-block visible-sm-inline-block\"\n >{{ column.label | translate }}</small\n >\n <span>{{ dp.target }}</span>\n </div>\n </div>\n }\n @case ('current') {\n <div\n class=\"col-md-2 text-right-md flex-grow\"\n [attr.data-cy]=\"'datapointlist-' + column.id\"\n >\n <div class=\"d-flex a-i-center d-contents-md p-t-4 p-b-4 separator-bottom\">\n <small\n class=\"text-label-small flex-grow visible-xs-inline-block visible-sm-inline-block\"\n >{{ column.label | translate }}</small\n >\n @let ariaLabel =\n 'Last updated: {{ date }}'\n | translate: { date: dp.timestamp | c8yDate: 'medium' };\n <span\n [class]=\"dp.currentValue | applyRangeClass: getRangeValues(dp)\"\n [title]=\"dp.timestamp | c8yDate: 'medium'\"\n [attr.aria-label]=\"ariaLabel\"\n >\n {{ dp.currentValue | number: dp.currentFractionSize }}\n </span>\n </div>\n </div>\n }\n @case ('diff') {\n <div\n class=\"col-md-1 text-right-md flex-grow\"\n [attr.data-cy]=\"'datapointlist-' + column.id\"\n >\n <div class=\"d-flex a-i-center d-contents-md p-t-4 p-b-4 separator-bottom\">\n <small\n class=\"text-label-small flex-grow visible-xs-inline-block visible-sm-inline-block\"\n >{{ column.label | translate }}</small\n >\n <span>\n {{ dp.diffValue | number: dp.diffFractionSize }}\n </span>\n </div>\n </div>\n }\n @case ('diffPercentage') {\n <div\n class=\"col-md-1 text-right-md flex-grow\"\n [attr.data-cy]=\"'datapointlist-' + column.id\"\n >\n <div class=\"d-flex a-i-center d-contents-md p-t-4 p-b-4 separator-bottom\">\n <small\n class=\"text-label-small flex-grow visible-xs-inline-block visible-sm-inline-block\"\n >{{ column.label | translate }}</small\n >\n <span>\n {{ dp.diffPercentValue | number: dp.diffPercentFractionSize }}\n </span>\n </div>\n </div>\n }\n @case ('asset') {\n <div\n class=\"col-md-3 flex-grow\"\n [attr.data-cy]=\"'datapo