UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

922 lines (914 loc) 93 kB
import * as i0 from '@angular/core'; import { Injectable, Pipe, EventEmitter, Output, Input, Directive, ChangeDetectionStrategy, Component, signal, ViewChild, Optional } from '@angular/core'; import * as i1$3 from '@angular/forms'; import { ReactiveFormsModule, Validators } from '@angular/forms'; import * as i1$2 from '@c8y/ngx-components'; import { DynamicComponentAlertAggregator, DynamicComponentAlert, CommonModule, DocsModule, DynamicComponentModule, ListGroupModule, DismissAlertStrategy, CoreModule } from '@c8y/ngx-components'; import * as i2$1 from '@c8y/ngx-components/context-dashboard'; import * as i5$1 from '@c8y/ngx-components/datapoint-selector'; import { DatapointSelectorModule } from '@c8y/ngx-components/datapoint-selector'; import * as i5 from '@c8y/ngx-components/global-context'; import { REFRESH_OPTION, GlobalContextWidgetWrapperComponent, AGGREGATION_LABELS, AGGREGATION_VALUES_ARR } from '@c8y/ngx-components/global-context'; import { INTERVAL_VALUES } from '@c8y/ngx-components/interval-picker'; import { PopoverModule } from 'ngx-bootstrap/popover'; import { BehaviorSubject, Subject, debounceTime } from 'rxjs'; import { skip, distinctUntilChanged, takeUntil, debounceTime as debounceTime$1 } from 'rxjs/operators'; import { gettext } from '@c8y/ngx-components/gettext'; import * as i1 from '@c8y/ngx-components/datapoints-export-selector'; import { DatapointsExportSelectorComponent } from '@c8y/ngx-components/datapoints-export-selector'; import * as i1$1 from '@ngx-translate/core'; import { merge, isEqual } from 'lodash-es'; import { aggregationType } from '@c8y/client'; import * as i2 from '@angular/common'; const DEFAULT_DPT_REFRESH_INTERVAL_VALUE = 30_000; /** * Represents the legacy's global time context date selection of the widget. */ const DATE_SELECTION_VALUES = { dashboard_context: 'dashboard_context', config: 'config', view_and_config: 'view_and_config' }; const DATE_SELECTION_VALUES_ARR = [ DATE_SELECTION_VALUES.dashboard_context, DATE_SELECTION_VALUES.config, DATE_SELECTION_VALUES.view_and_config ]; const DATE_SELECTION_LABELS = { config: gettext('Widget configuration'), view_and_config: gettext('Widget and widget configuration'), dashboard_context: gettext('Dashboard time range') }; const REFRESH_INTERVAL_VALUES_ARR = [5_000, 10_000, 15_000, 30_000, 60_000]; const RENDER_TYPES_LABELS = { min: gettext('Minimum'), max: gettext('Maximum'), area: gettext('Area') }; const INTERVAL_VALUES_ARR = [ INTERVAL_VALUES.minutes, INTERVAL_VALUES.hours, INTERVAL_VALUES.days, INTERVAL_VALUES.weeks, INTERVAL_VALUES.months, INTERVAL_VALUES.custom ]; const TIME_RANGE_INTERVAL_LABELS = { minutes: gettext('Last minute'), hours: gettext('Last hour'), days: gettext('Last day'), weeks: gettext('Last week'), months: gettext('Last month'), custom: gettext('Custom') }; const DURATION_OPTIONS = [ { id: INTERVAL_VALUES.minutes, label: TIME_RANGE_INTERVAL_LABELS.minutes, unit: INTERVAL_VALUES.minutes, amount: 1 }, { id: INTERVAL_VALUES.hours, label: TIME_RANGE_INTERVAL_LABELS.hours, unit: INTERVAL_VALUES.hours, amount: 1 }, { id: INTERVAL_VALUES.days, label: TIME_RANGE_INTERVAL_LABELS.days, unit: INTERVAL_VALUES.days, amount: 1 }, { id: INTERVAL_VALUES.weeks, label: TIME_RANGE_INTERVAL_LABELS.weeks, unit: INTERVAL_VALUES.weeks, amount: 1 }, { id: INTERVAL_VALUES.months, label: TIME_RANGE_INTERVAL_LABELS.months, unit: INTERVAL_VALUES.months, amount: 1 }, { id: INTERVAL_VALUES.custom, label: TIME_RANGE_INTERVAL_LABELS.custom } ]; function mapToSourceValueObject([key, value]) { return { key, value }; } class DatapointsTableViewService { constructor(dataFetchingService) { this.dataFetchingService = dataFetchingService; } /** * Filters out inactive data points from the given array. * * @param datapoints - The array of data points to filter. * @returns An array of data points that are active. */ filterOutInactiveDatapoints(datapoints) { return datapoints.filter((datapoint) => datapoint.__active); } hasMultipleDatapoints(datapoints) { const ids = datapoints.map(dp => dp.__target.id); return ids.length > 1; } /** * Returns a map of active data points device IDs with their corresponding series. * * Example output: * ```typescript * new Map([ * [ * "844657202", * [ * "c8y_Temperature.T" * ] * ], * [ * "32666427", * [ * "c8y_Battery.Battery" * ] * ] * ]); * ``` * @param datapoints - An array of data points. * @returns A map where the key is the data point ID and the value is an array of data point series. */ groupSeriesByDeviceId(activeDatapoints) { return activeDatapoints.reduce((map, { fragment, series, __target: { id } }) => { const value = `${fragment}.${series}`; const existingValue = map.get(id) ?? []; map.set(id, [...existingValue, value]); return map; }, new Map()); } /** * Retrieves the active data points series data and returns it as a map. * * @param datapointsValuesDataMap - A map of data point sources with their associated series. * @param config - The configuration of the data points table. * @param roundSeconds - Whether to round the seconds or not. * If true, the seconds will be rounded to 0. * If false, the seconds will be displayed as they are. * @returns A Promise that resolves to a Map object with data point IDs as keys and DataObject as values or undefined when all series has forbidden access. */ async getAllActiveSeriesDataMap(datapointsValuesDataMap, config, roundSeconds) { const promises = Array.from(datapointsValuesDataMap).map(async ([source, series]) => { const params = { dateFrom: config.dateTimeContext.dateFrom, dateTo: config.dateTimeContext.dateTo, source, series, aggregationType: config.aggregation }; const { data, res } = await this.dataFetchingService.fetchSeriesData(params, roundSeconds); return { source, data, res }; }); const results = await Promise.all(promises); const allSeriesHasForbiddenAccess = results.every(item => item.res?.status === 403); if (allSeriesHasForbiddenAccess) { throw new Error('Access forbidden: All series have a 403 status code'); } const filteredResults = this.filterOutElementsWithForbiddenResponses(results); const resultMap = new Map(); filteredResults.forEach(result => resultMap.set(result.source, result.data)); return resultMap; } /** * Creates an array of DatapointsWithValues based on the provided datapoints and datapointsSeriesDataMap. * * Finds an index of a current data point within series object and based on that index filters values array. * * @param datapoints - An array of data points. * @param datapointsSeriesDataMap - A map containing series data for data points. * @returns An array of DatapointsWithValues. */ getDatapointsWithValues(datapoints, datapointsSeriesDataMap) { return datapoints.map((dp) => { const seriesData = datapointsSeriesDataMap.get(dp.__target.id); if (!seriesData) { return { ...dp, values: {} }; } // Find an index of a corresponding datapoint data, within series object. const datapointSeriesArrayIndex = seriesData.series.findIndex(s => s.name === dp.series && s.type === dp.fragment); const valuesFilteredByDatapointSeriesArrayIndex = Object.fromEntries(Object.entries(seriesData.values).map(([key, arr]) => [ key, arr.filter((_, index) => index === datapointSeriesArrayIndex) ])); return { ...dp, seriesUnit: seriesData.series[datapointSeriesArrayIndex]?.unit, values: valuesFilteredByDatapointSeriesArrayIndex }; }); } /** * Creates the column headers for the devices in the data points table. * * @param datapointsWithValues - An array of data points. * @returns An array of column headers for the devices. */ getColumnHeaders(datapointsWithValues) { return datapointsWithValues.map(({ __target: { name }, label, renderType, unit }) => { return { deviceName: name, label, renderType, unit }; }); } mapDatapointsWithValuesToList(datapointsWithValues) { return datapointsWithValues.flatMap(dp => { if (!dp.values) { return []; } return Object.entries(dp.values).flatMap(([date, valuesArray]) => { const value = this.findMinMaxValues(valuesArray); if (value == null) { return []; } return [ { dateAndTime: date, deviceName: dp.__target.name, fragment: dp.fragment, label: dp.label, redRangeMax: dp.redRangeMax, redRangeMin: dp.redRangeMin, renderType: dp.renderType, series: dp.series, value: value, yellowRangeMax: dp.yellowRangeMax, yellowRangeMin: dp.yellowRangeMin } ]; }); }); } /** * Finds the overall minimum and maximum values from an array of objects containing 'min' and 'max' properties. * * If the array contains only one object, that object's 'min' and 'max' values will be returned. * * @param valuesArray - An array with objects, where each contains 'min' and 'max' properties. * @returns An object with the smallest 'min' and largest 'max' values found in the array. * * @example * const values = [ * { min: 1, max: 10 } * ]; * * const result = findMinMaxValues(values); * // result is { min: 1, max: 10 } */ findMinMaxValues(valuesArray) { const validItems = valuesArray.filter((item) => item && item.min != null && item.max != null); if (validItems.length === 0) { return null; } const initialValue = { min: validItems[0].min, max: validItems[0].max }; return validItems.reduce((acc, item) => ({ min: Math.min(acc.min, item.min), max: Math.max(acc.max, item.max) }), initialValue); } /** * Groups a list of data points by date and device, based on given references. * * @param dataList - The list of data points to be grouped. * @param references - The column headers that serve as references for grouping. * @returns An array of grouped data points, where each group corresponds to a unique date and device. */ groupByDateAndDevice(dataList, references) { const map = this.generateDataPointMap(dataList, references); return this.mergeDatapoints(map); } /** * Generates and populates a map with data points. * * This function processes the provided data points and organizes them into a map structure * where each key is a unique combination of date and device identifiers, and the value is an * array of data points (or null values) associated with that key. This structured data is then * used in the data point table to render the data appropriately. * * @param dataList - The list of data point table items to be processed. * @param columnsHeadersReferences - The list of column headers used to determine the order and structure of the map values. * @returns A map where the key is a datapoint identifier containing the date and device name, and the value is an array of data point table items or null. */ generateDataPointMap(dataList, columnsHeadersReferences) { // Map to store the data points indexed by a unique identifier. const map = new Map(); dataList.forEach(obj => { // Generate a unique identifier for each data point using the date and device name. const dateKey = this.toISOFormat(obj.dateAndTime); const datapointIdentifier = `${dateKey}_${obj.deviceName}`; // Initialize the map entry if it does not exist with a unique identifier. if (!map.has(datapointIdentifier)) { map.set(datapointIdentifier, Array(columnsHeadersReferences.length).fill(null)); } // Find the index of the reference that matches the current data point. const matchingColumnIndex = columnsHeadersReferences.findIndex(ref => ref.deviceName === obj.deviceName && ref.label === obj.label); // Update the map entry with the data point. const tableItem = map.get(datapointIdentifier); if (tableItem) { tableItem[matchingColumnIndex] = { ...obj }; } }); return map; } /** * Merges the data points from the given map into an array of grouped data point table items. * * @param map - The map containing the data points to be merged. * @returns An array of grouped data point table items. */ mergeDatapoints(map) { const mergedData = []; map.forEach((values, datapointIdentifier) => { const [dateKey, deviceName] = datapointIdentifier.split('_'); const validDataPoint = values.some(item => item && Object.keys(item.value).length > 0); if (validDataPoint) { mergedData.push({ dateAndTime: dateKey, deviceName: deviceName, rowItems: values.map(item => (item ? { ...item } : null)) }); } }); return mergedData; } sortDataByDateDescending(data) { return data.sort((a, b) => new Date(b.dateAndTime).getTime() - new Date(a.dateAndTime).getTime()); } /** * Prepares the updated time range based on the selected interval. * * In case of a 'custom' interval or no quantity, the original date range is returned. * * @param interval - The selected interval type. * @returns An object containing the `dateFrom` and `dateTo` in ISO string format. */ prepareTimeRange(interval, dateFromInput, dateToInput) { if (!interval || interval === INTERVAL_VALUES.custom) { return { dateFrom: dateFromInput, dateTo: dateToInput }; } const selected = DURATION_OPTIONS.find(selectedDuration => selectedDuration.id === interval); if (!selected?.amount) { return { dateFrom: dateFromInput, dateTo: dateToInput }; } const now = new Date(); return { dateFrom: this.subtractTime(now, selected.amount, selected.unit).toISOString(), dateTo: now.toISOString() }; } /** * Subtracts an amount of time from a given date. * * @param date - The original date. * @param amount - The amount of time units to subtract. * @param unit - The unit of time to subtract (e.g., minutes, hours, days, weeks, months). * @returns A new date with the specified time subtracted. */ subtractTime(date, amount, unit) { const newDate = new Date(date); switch (unit) { case INTERVAL_VALUES.minutes: newDate.setUTCMinutes(newDate.getUTCMinutes() - amount); break; case INTERVAL_VALUES.hours: newDate.setUTCHours(newDate.getUTCHours() - amount); break; case INTERVAL_VALUES.days: newDate.setUTCDate(newDate.getUTCDate() - amount); break; case INTERVAL_VALUES.weeks: newDate.setUTCDate(newDate.getUTCDate() - amount * 7); break; case INTERVAL_VALUES.months: this.subtractMonthsAndAdjustDay(newDate, amount); break; } return newDate; } getSeriesWithoutPermissionToRead(activeDatapointsSeriesData, activeDatapointsIdsWithSeries) { if (!activeDatapointsSeriesData) { // Returns all activeDatapointsIdsWithSeries entries if activeDatapointsSeriesData is undefined. // It means that the user does not have permission to see any of the selected datapoints data. return Array.from(activeDatapointsIdsWithSeries, mapToSourceValueObject); } const availableSources = new Set(activeDatapointsSeriesData.keys()); return Array.from(activeDatapointsIdsWithSeries) .filter(([source]) => !availableSources.has(source)) .map(mapToSourceValueObject); } hasSecondsAndMillisecondsEqualZero(timeString) { if (!timeString) { return false; } const date = new Date(timeString); if (isNaN(date.getTime())) { return false; } return date.getUTCSeconds() === 0 && date.getUTCMilliseconds() === 0; } /** * Converts a date string to ISO format. * * @param dateStr - The date string to convert. * @returns The ISO format of the given date string. */ toISOFormat(dateStr) { return new Date(dateStr).toISOString(); } filterOutElementsWithForbiddenResponses(seriesDataWithResponse) { return seriesDataWithResponse.filter(item => { return !(item.res?.status === 403); }); } subtractMonthsAndAdjustDay(date, monthsToSubtract) { const currentMonth = date.getUTCMonth(); const expectedTargetMonth = this.calculateTargetMonth(currentMonth, monthsToSubtract); date.setUTCMonth(currentMonth - monthsToSubtract); const actualMonth = date.getUTCMonth(); const dayDoesNotExistInTargetMonth = actualMonth !== expectedTargetMonth; if (dayDoesNotExistInTargetMonth) { this.setToLastDayOfPreviousMonth(date); } } /** * Calculates the target month number (0-11) after subtracting months from the current month. * Handles negative month numbers by normalizing them to the valid 0-11 range. * * Examples: * - January(0) - 1 month = December(11) * - March(2) - 4 months = November(10) * - December(11) - 1 month = November(10) * * @param currentMonth - Current month (0-11, where 0 is January) * @param monthsToSubtract - Number of months to subtract * @returns Normalized month number in range 0-11 */ calculateTargetMonth(currentMonth, monthsToSubtract) { return (currentMonth - monthsToSubtract + 12) % 12; } /** * Sets the date to the last day of the previous month. * Using 0 as dateValue makes JavaScript automatically calculate * last day of previous month, per JavaScript Date API behavior. * @param date - Date to modify */ setToLastDayOfPreviousMonth(date) { date.setUTCDate(0); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DatapointsTableViewService, deps: [{ token: i1.DataFetchingService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DatapointsTableViewService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DatapointsTableViewService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.DataFetchingService }] }); /** * A pipe that adjusts the aggregated time range based on the aggregation type. * * ```html * '9:00' | adjustAggregatedTimeRange: config.aggregation (e.g.:HOURLY) * ``` * The output will be '9:00-10:00'. */ class AdjustAggregatedTimeRangePipe { /** * Transforms the input time based on the aggregation type. * @param inputTime The input time string. * @param aggregationType The type of aggregation (optional). * @returns The transformed time string. */ transform(inputTime, aggregationType$1) { if (!aggregationType$1) { return inputTime; } if (aggregationType$1 === aggregationType.DAILY) { return ''; } const date = this.createDateFromInput(inputTime); const isTwelveHoursFormat = this.isTwelveHoursFormat(inputTime); switch (aggregationType$1) { case aggregationType.HOURLY: return this.getHourlyTimeRange(date, isTwelveHoursFormat); case aggregationType.MINUTELY: return this.getMinutelyTimeRange(date, isTwelveHoursFormat); default: throw new Error('Unsupported aggregation type'); } } /** * Creates a date object from the input time string. * @param inputTime The input time string. * @returns The created Date object. */ createDateFromInput(inputTime) { const defaultDate = '1970-01-01 '; const isPM = /PM/i.test(inputTime); const cleanedTime = inputTime.replace(/AM|PM/i, '').trim(); this.validateTimeFormat(cleanedTime, inputTime); const dateTimeString = `${defaultDate}${cleanedTime}`; const date = new Date(dateTimeString); if (isNaN(date.getTime())) { throw new Error('Invalid input time'); } return this.adjustForPMTime(date, isPM); } /** * Validates if the time string matches the required format and has valid values. * @param time The time string to validate. * @param originalInput The original input string (including AM/PM if present). * @throws Error if the time format is invalid or values are out of range. */ validateTimeFormat(time, originalInput) { const parts = time.split(':'); this.validateTimeParts(parts); const [hoursStr, minutesStr, secondsStr] = parts; this.validateTimeDigits(hoursStr, minutesStr, secondsStr); const { hours, minutes, seconds } = this.parseTimeComponents(hoursStr, minutesStr, secondsStr); this.validateTimeRanges(hours, minutes, seconds); this.validateTimeFormat24Hour(hours, originalInput); } validateTimeParts(parts) { if (parts.length < 2 || parts.length > 3) { throw new Error('Invalid input time'); } } validateTimeDigits(hoursStr, minutesStr, secondsStr) { if (!this.isValidNumberString(hoursStr) || !this.isValidNumberString(minutesStr) || (secondsStr !== undefined && !this.isValidNumberString(secondsStr))) { throw new Error('Invalid input time'); } } parseTimeComponents(hoursStr, minutesStr, secondsStr) { return { hours: Number(hoursStr), minutes: Number(minutesStr), seconds: secondsStr ? Number(secondsStr) : 0 }; } validateTimeRanges(hours, minutes, seconds) { if (hours > 23 || hours < 0 || minutes > 59 || minutes < 0 || seconds > 59 || seconds < 0) { throw new Error('Invalid input time'); } } validateTimeFormat24Hour(hours, originalInput) { if (hours > 12 && this.hasAmPm(originalInput)) { throw new Error('Invalid input time'); } } /** * Checks if string contains only digits and is 1-2 characters long. * @param value String to check * @returns boolean indicating if string is valid */ isValidNumberString(value) { return (value.length > 0 && value.length <= 2 && value.split('').every(char => char >= '0' && char <= '9')); } /** * Checks if the input time has AM/PM markers. * @param input The input time string to check. * @returns boolean indicating if the input contains AM/PM. */ hasAmPm(input) { return /AM|PM/i.test(input); } /** * Adjusts the date for PM times by adding 12 hours when necessary. * @param date The date object to adjust. * @param isPM Boolean indicating if the time is PM. * @returns The adjusted Date object. */ adjustForPMTime(date, isPM) { const hours = date.getHours(); if (isPM && hours < 12) { date.setHours(hours + 12); } else if (!isPM && hours === 12) { date.setHours(0); } return date; } /** * Checks if the input time is in twelve hours format. * @param inputTime The input time string. * @returns True if the input time is in twelve hours format, false otherwise. */ isTwelveHoursFormat(inputTime) { return /AM|PM/i.test(inputTime); } /** * Gets the hourly time range for the given date. * @param date The date object. * @param twelveHoursFormat Indicates whether to use twelve hours format. * @returns The hourly time range string. */ getHourlyTimeRange(date, twelveHoursFormat) { const nextHour = new Date(date.getTime()); nextHour.setHours(date.getHours() + 1); return `${this.formatTime(date, twelveHoursFormat, true)}-${this.formatTime(nextHour, twelveHoursFormat, true)}`; } /** * Gets the minutely time range for the given date. * @param date The date object. * @param twelveHoursFormat Indicates whether to use twelve hours format. * @returns The minutely time range string. */ getMinutelyTimeRange(date, twelveHoursFormat) { const nextMinute = new Date(date.getTime()); nextMinute.setMinutes(date.getMinutes() + 1); return `${this.formatTime(date, twelveHoursFormat, false)}-${this.formatTime(nextMinute, twelveHoursFormat, false)}`; } /** * Formats the given date into a time string. * @param date The date to format. * @param usePeriod Indicates whether to include the period (AM/PM) in the formatted time. * @param useHourOnly Indicates whether to include only the hour part in the formatted time. * @returns The formatted time string. */ formatTime(date, usePeriod, useHourOnly) { const hours = date.getHours(); const minutes = date.getMinutes().toString().padStart(2, '0'); if (usePeriod) { const period = hours >= 12 ? 'PM' : 'AM'; const formattedHours = hours % 12 === 0 ? 12 : hours % 12; return `${formattedHours}:${useHourOnly ? '00' : minutes} ${period}`; } else { return `${hours.toString().padStart(2, '0')}:${useHourOnly ? '00' : minutes}`; } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AdjustAggregatedTimeRangePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: AdjustAggregatedTimeRangePipe, isStandalone: true, name: "adjustAggregatedTimeRange" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AdjustAggregatedTimeRangePipe, decorators: [{ type: Pipe, args: [{ name: 'adjustAggregatedTimeRange', standalone: true }] }] }); /** * Applies CSS classes based on the value's range. */ class ApplyRangeClassPipe { /** * Transforms the input value based on the specified ranges. * * @param value - Initial value used to determine the CSS class. * @param ranges - An object containing the min and max range values for yellow and red colors. * @returns The CSS class to be applied. */ transform(value, ranges) { if (value == null) { return; } if (value >= ranges.yellowRangeMin && value < ranges.yellowRangeMax) { return 'text-warning'; } else if (value >= ranges.redRangeMin && value <= ranges.redRangeMax) { return 'text-danger'; } return 'default'; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ApplyRangeClassPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: ApplyRangeClassPipe, isStandalone: true, name: "applyRangeClass" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ApplyRangeClassPipe, decorators: [{ type: Pipe, args: [{ name: 'applyRangeClass', standalone: true }] }] }); /** * Creates a column header title message. * * ```html * title="{{ header | columnTitle }}" * ``` * The output will be e.g.: 'c8y_Temperature → T [T] (Area)'. */ class ColumnTitlePipe { constructor(translateService) { this.translateService = translateService; } /** * Transforms the column header into a formatted string with label and optionally unit and render type. * * @param columnHeader - The column header object. * @returns The formatted string with label, unit, and render type. */ transform(columnHeader) { const label = columnHeader.label.trim(); const unit = columnHeader.unit ? `[${columnHeader.unit.trim()}]` : ''; const renderType = columnHeader.renderType ? this.translateService.instant(RENDER_TYPES_LABELS[columnHeader.renderType]) : ''; if (!renderType) { return `${label} ${unit}`.trim(); } return `${label} ${unit} (${renderType})`.trim(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ColumnTitlePipe, deps: [{ token: i1$1.TranslateService }], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: ColumnTitlePipe, isStandalone: true, name: "columnTitle" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ColumnTitlePipe, decorators: [{ type: Pipe, args: [{ name: 'columnTitle', standalone: true }] }], ctorParameters: () => [{ type: i1$1.TranslateService }] }); /** * Directive to listen for scroll events on a virtual scroll container. * Emits `scrolled` and `scrolledToTop` events. * * The directive listens for scroll events on a virtual scroll container. * - When the container is scrolled by at least 50 pixels (or a custom threshold), the `scrolled` event is emitted. * - When the container is scrolled to the top, the `scrolledToTop` event is emitted. * */ class VirtualScrollListenerDirective { constructor(el, ngZone) { this.el = el; this.ngZone = ngZone; /** * Pixel threshold for emitting the scrolled event. */ this.scrollThreshold = 50; /** * Event emitted when the virtual scroll container is scrolled by at least 50 pixels. */ this.scrolled = new EventEmitter(); /** * Event emitted when the virtual scroll container is scrolled to the top. */ this.scrolledToTop = new EventEmitter(); this.lastScrollTop = 0; } ngAfterViewInit() { this.virtualScrollContainer = this.el.nativeElement.querySelector('.cdk-virtual-scroll-viewport'); if (this.virtualScrollContainer) { this.ngZone.runOutsideAngular(() => { this.virtualScrollContainer.addEventListener('scroll', this.onScroll.bind(this)); }); } } ngOnDestroy() { if (this.virtualScrollContainer) { this.virtualScrollContainer.removeEventListener('scroll', this.onScroll.bind(this)); } } /** * Handles the scroll event. Emits `scrolled` event if scrolled by at least `scrollThreshold` pixels * and `scrolledToTop` event if scrolled to the top. * @param event - The scroll event. */ onScroll(event) { const target = event.target; const scrollTop = target.scrollTop; if (Math.abs(scrollTop - this.lastScrollTop) > this.scrollThreshold) { this.lastScrollTop = scrollTop; this.ngZone.run(() => this.scrolled.emit()); } if (scrollTop === 0) { this.ngZone.run(() => this.scrolledToTop.emit()); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: VirtualScrollListenerDirective, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.15", type: VirtualScrollListenerDirective, isStandalone: true, selector: "[c8yVirtualScrollListener]", inputs: { scrollThreshold: "scrollThreshold" }, outputs: { scrolled: "scrolled", scrolledToTop: "scrolledToTop" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: VirtualScrollListenerDirective, decorators: [{ type: Directive, args: [{ selector: '[c8yVirtualScrollListener]', standalone: true }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.NgZone }], propDecorators: { scrollThreshold: [{ type: Input }], scrolled: [{ type: Output }], scrolledToTop: [{ type: Output }] } }); class DynamicColumnDirective { constructor(el, renderer) { this.el = el; this.renderer = renderer; } ngOnInit() { this.updateColumnClass(); } updateColumnClass() { let className = ''; switch (this.numberOfColumns) { case 1: className = 'col-md-12'; break; case 2: className = 'col-md-6'; break; default: if (this.numberOfColumns >= 3) { className = 'col-md-3'; } break; } if (className) { this.renderer.addClass(this.el.nativeElement, className); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DynamicColumnDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.15", type: DynamicColumnDirective, isStandalone: true, selector: "[c8yDynamicColumn]", inputs: { numberOfColumns: ["c8yDynamicColumn", "numberOfColumns"] }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DynamicColumnDirective, decorators: [{ type: Directive, args: [{ selector: '[c8yDynamicColumn]', standalone: true }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { numberOfColumns: [{ type: Input, args: ['c8yDynamicColumn'] }] } }); class DatapointsTableComponent { constructor() { this.isScrolling = new EventEmitter(); this.hasNoPermissionsToReadAnyMeasurement = false; this.missingAllPermissionsAlert = new DynamicComponentAlertAggregator(); /** * Default fraction size format for numbers with decimal places. */ this.fractionSize = '1.2-2'; } ngOnChanges() { if (typeof this.decimalPlaces === 'number' && !Number.isNaN(this.decimalPlaces)) { this.fractionSize = `1.${this.decimalPlaces}-${this.decimalPlaces}`; } if (!this.seriesWithoutPermissionToReadCount) { return; } this.missingAllPermissionsAlert.clear(); this.handleNoPermissionErrorMessage(); } onListScrolled() { this.isScrolling.emit(true); } onListScrolledToTop() { this.isScrolling.emit(false); } getRangeValues(row) { return { yellowRangeMin: row.yellowRangeMin, yellowRangeMax: row.yellowRangeMax, redRangeMin: row.redRangeMin, redRangeMax: row.redRangeMax }; } /** * Determines the fraction size format based on whether the number is an integer or has decimal places. * * @param value - The number to be formatted. * @returns Returns '1.0-0' if the number is an integer, otherwise returns the current fraction size. */ getFractionSize(value) { return value % 1 === 0 ? '1.0-0' : this.fractionSize; } handleNoPermissionErrorMessage() { this.hasNoPermissionsToReadAnyMeasurement = this.seriesWithoutPermissionToReadCount === this.devicesColumnHeaders.length; if (this.hasNoPermissionsToReadAnyMeasurement) { 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.15", ngImport: i0, type: DatapointsTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: DatapointsTableComponent, isStandalone: true, selector: "c8y-datapoints-table", inputs: { aggregationType: "aggregationType", datapointsTableItems: "datapointsTableItems", devicesColumnHeaders: "devicesColumnHeaders", decimalPlaces: "decimalPlaces", hasMultipleDatapoints: "hasMultipleDatapoints", isLoading: "isLoading", seriesWithoutPermissionToReadCount: "seriesWithoutPermissionToReadCount" }, outputs: { isScrolling: "isScrolling" }, host: { classAttribute: "d-col flex-grow" }, usesOnChanges: true, ngImport: i0, template: "<ng-container *ngIf=\"!hasNoPermissionsToReadAnyMeasurement; else missingAllPermissions\">\n <div class=\"c8y-cq-440\">\n <div\n class=\"hidden-xs c8y-list__item c8y-list--timeline hidden-cq\"\n [ngClass]=\"{ 'separator-top-bottom': devicesColumnHeaders.length > 0 }\"\n >\n <div class=\"d-flex container-fluid\">\n <div class=\"c8y-list--timeline__item__date\"></div>\n <div class=\"c8y-list__item__block flex-grow min-width-0\">\n <div class=\"c8y-list__item__body\">\n <div class=\"d-flex row\">\n <ng-container *ngIf=\"hasMultipleDatapoints\">\n <div\n class=\"min-width-0\"\n [title]=\"'Device' | translate\"\n [c8yDynamicColumn]=\"devicesColumnHeaders.length\"\n >\n <span class=\"text-medium text-truncate\">\n {{ 'Device' | translate }}\n </span>\n </div>\n </ng-container>\n <!-- Data points column headers -->\n <ng-container *ngFor=\"let header of devicesColumnHeaders\">\n <div\n class=\"min-width-0\"\n title=\"{{ header | columnTitle }}\"\n [c8yDynamicColumn]=\"devicesColumnHeaders.length\"\n >\n <span class=\"text-medium text-truncate\">\n {{ header.label }} {{ header.unit }}\n </span>\n </div>\n </ng-container>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n <!-- The record list -->\n <ng-container *ngIf=\"!isLoading; else loading\">\n <ng-container *ngIf=\"datapointsTableItems.length; else emptyState\">\n <c8y-list-group\n class=\"p-t-8 flex-grow c8y-cq-440\"\n c8yVirtualScrollListener\n (scrolled)=\"onListScrolled()\"\n (scrolledToTop)=\"onListScrolledToTop()\"\n >\n <c8y-li-timeline\n *c8yFor=\"\n let tableItem of { data: datapointsTableItems, res: null };\n enableVirtualScroll: true;\n virtualScrollElementSize: 48;\n virtualScrollStrategy: 'fixed'\n \"\n >\n {{ tableItem.dateAndTime | c8yDate: 'mediumDate' }}\n {{\n tableItem.dateAndTime\n | c8yDate: 'mediumTime'\n | adjustAggregatedTimeRange: aggregationType\n }}\n <c8y-li>\n <c8y-li-body>\n <div class=\"d-flex row\">\n <div\n class=\"min-width-0\"\n [c8yDynamicColumn]=\"devicesColumnHeaders.length\"\n *ngIf=\"devicesColumnHeaders.length > 1\"\n [attr.data-label]=\"'Device' | translate\"\n >\n <div\n class=\"text-truncate\"\n title=\"{{ tableItem.deviceName }}\"\n >\n {{ tableItem.deviceName }}\n </div>\n </div>\n <!-- Data point value row cells -->\n <ng-container *ngFor=\"let row of tableItem.rowItems\">\n <ng-container *ngIf=\"row !== null; else emptyRowContent\">\n <ng-container [ngSwitch]=\"row.renderType\">\n <div\n [c8yDynamicColumn]=\"devicesColumnHeaders.length\"\n [ngClass]=\"row.value.min ?? null | applyRangeClass: getRangeValues(row)\"\n *ngSwitchCase=\"'min'\"\n [attr.data-label]=\"row.label\"\n >\n <div\n class=\"text-truncate\"\n title=\"{{ row.value.min ?? '' | number: getFractionSize(row.value.min) }}\"\n >\n {{ row.value.min ?? '' | number: getFractionSize(row.value.min) }}\n </div>\n </div>\n <div\n class=\"col-md-4\"\n [ngClass]=\"row.value.max ?? null | applyRangeClass: getRangeValues(row)\"\n *ngSwitchCase=\"'max'\"\n >\n <div\n class=\"text-truncate\"\n title=\"{{ row.value.max ?? '' | number: getFractionSize(row.value.max) }}\"\n >\n {{ row.value.max ?? '' | number: getFractionSize(row.value.max) }}\n </div>\n </div>\n <div\n [c8yDynamicColumn]=\"devicesColumnHeaders.length\"\n *ngSwitchCase=\"'area'\"\n >\n <span\n class=\"text-truncate\"\n title=\"{{ row.value.min ?? '' | number: getFractionSize(row.value.min) }}\"\n [ngClass]=\"row.value.min ?? null | applyRangeClass: getRangeValues(row)\"\n >\n {{ row.value.min ?? '' | number: getFractionSize(row.value.min) }}\n </span>\n ...\n <span\n class=\"text-truncate\"\n title=\"{{ row.value.max ?? '' | number: getFractionSize(row.value.max) }}\"\n [ngClass]=\"row.value.max ?? null | applyRangeClass: getRangeValues(row)\"\n >\n {{ row.value.max ?? '' | number: getFractionSize(row.value.max) }}\n </span>\n </div>\n <div\n [c8yDynamicColumn]=\"devicesColumnHeaders.length\"\n *ngSwitchDefault\n >\n <span\n class=\"text-truncate\"\n title=\"{{ row.value.min ?? '' | number: getFractionSize(row.value.min) }}\"\n [ngClass]=\"row.value.min ?? null | applyRangeClass: getRangeValues(row)\"\n >\n {{ row.value.min ?? '' | number: getFractionSize(row.value.min) }}\n </span>\n </div>\n </ng-container>\n </ng-container>\n <ng-template #emptyRowContent>\n <div [c8yDynamicColumn]=\"devicesColumnHeaders.length\"></div>\n </ng-template>\n </ng-container>\n </div>\n </c8y-li-body>\n </c8y-li>\n </c8y-li-timeline>\n </c8y-list-group>\n </ng-container>\n </ng-container>\n</ng-container>\n<ng-template #loading>\n <c8y-loading></c8y-loading>\n</ng-template>\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-table-list--empty-state\"\n >\n <p c8y-guide-docs>\n <small translate>\n Find out more in the\n <a c8y-guide-href=\"/docs/cockpit/widgets-collection/#data-point-table\">\n user documentation\n </a>\n .\n </small>\n </p>\n </c8y-ui-empty-state>\n </div>\n</ng-template>\n<ng-template #missingAllPermissions>\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</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: i1$2.EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "directive", type: i1$2.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i2.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i2.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "directive", type: i1$2.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: i1$2.LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "message"] }, { kind: "ngmodule", type: DocsModule }, { kind: "directive", type: i1$2.GuideHrefDirective, selector: "[c8y-guide-href]", inputs: ["c8y-guide-href"] }, { kind: "component", type: i1$2.GuideDocsComponent, selector: "[c8y-guide-docs]" }, { kind: "directive", type: DynamicColumnDirective, selector: "[c8yDynamicColumn]", inputs: ["c8yDynamicColumn"] }, { kind: "ngmodule", type: DynamicComponentModule }, { kind: "component", type: i1$2.DynamicComponentAlertsComponent, selector: "c8y-dynamic-component-alerts", inputs: ["alerts"] }, { kind: "ngmodule", type: ListGroupModule }, { kind: "component", type: i1$2.ListGroupComponent, selector: "c8y-list-group" }, { kind: "component", type: i1$2.ListItemComponent, selector: "c8y-list-item, c8y-li", inputs: ["active", "highlighted", "emptyActions", "dense", "collapsed", "selectable"], outputs: ["collapsedChange"] }