@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
588 lines (584 loc) • 73.6 kB
JavaScript
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