@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
922 lines (914 loc) • 93 kB
JavaScript
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"] }