@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
558 lines (554 loc) • 142 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, EventEmitter, inject, Component, ChangeDetectionStrategy, Input, Output, Pipe, ViewChild, Directive, signal } from '@angular/core';
import * as i3 from '@angular/forms';
import { ControlContainer, ReactiveFormsModule, FormControl, Validators } from '@angular/forms';
import * as i1$1 from '@c8y/ngx-components';
import { gettext, CoreModule, AGGREGATION_LABELS, AGGREGATION_VALUES_ARR, AGGREGATION_VALUES, IntervalBasedReload, CountdownIntervalComponent, CommonModule as CommonModule$1, CountdownIntervalModule, ListGroupModule, DynamicComponentAlertAggregator, DynamicComponentAlert, DocsModule, DynamicComponentModule, DismissAlertStrategy, globalAutoRefreshLoading } from '@c8y/ngx-components';
import * as i4 from '@c8y/ngx-components/context-dashboard';
import * as i6 from '@c8y/ngx-components/datapoint-selector';
import { DatapointSelectorModule } from '@c8y/ngx-components/datapoint-selector';
import * as i1 from '@c8y/ngx-components/datapoints-export-selector';
import { dateRangeValidator, DatapointsExportSelectorComponent } from '@c8y/ngx-components/datapoints-export-selector';
import { INTERVAL_VALUES } from '@c8y/ngx-components/interval-picker';
import * as i7 from 'ngx-bootstrap/popover';
import { PopoverModule } from 'ngx-bootstrap/popover';
import { Subject, takeUntil, merge, tap, BehaviorSubject, debounceTime } from 'rxjs';
import * as i2 from '@angular/common';
import { CommonModule } from '@angular/common';
import { aggregationType } from '@c8y/client';
import * as i1$2 from '@ngx-translate/core';
import * as i4$1 from 'ngx-bootstrap/tooltip';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
const DEFAULT_DPT_REFRESH_INTERVAL_VALUE = 30_000;
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 }
];
class DatapointsTableService {
constructor(dataFetchingService) {
this.dataFetchingService = dataFetchingService;
}
calculateDateRange(interval) {
const now = new Date();
const nowString = now.toISOString();
let dateFrom;
switch (interval) {
case INTERVAL_VALUES.minutes:
dateFrom = this.dataFetchingService.adjustDate(nowString, -1, true);
break;
case INTERVAL_VALUES.hours:
const minutesInAnHourNegative = -60;
dateFrom = this.dataFetchingService.adjustDate(nowString, minutesInAnHourNegative, true);
break;
case INTERVAL_VALUES.days:
const minutesInADayNegative = -24 * 60;
dateFrom = this.dataFetchingService.adjustDate(nowString, minutesInADayNegative, true);
break;
case INTERVAL_VALUES.weeks:
const minutesInAWeekNegative = -7 * 24 * 60;
dateFrom = this.dataFetchingService.adjustDate(nowString, minutesInAWeekNegative, true);
break;
case INTERVAL_VALUES.months:
const oneMonthAgo = new Date(now);
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
dateFrom = this.dataFetchingService.adjustDate(oneMonthAgo.toISOString(), 0, true);
break;
default:
throw new Error('Invalid time interval');
}
return {
dateFrom: dateFrom,
dateTo: this.dataFetchingService.adjustDate(nowString, 0, true)
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DatapointsTableService, deps: [{ token: i1.DataFetchingService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DatapointsTableService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DatapointsTableService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: i1.DataFetchingService }] });
class DateRangePickerComponent {
constructor() {
/**
* If set to true, the component will be reactive and will emit the updatedDate on every change.
* Otherwise, the component will use a non emitting variant of a template.
*/
this.isEmittingDateChange = false;
/**
* Determines the display of from and to date picker labels.
*/
this.showLabel = true;
this.updatedDate = new EventEmitter();
this.DATE_FROM = 'dateFrom';
this.DATE_TO = 'dateTo';
this.FROM_DATE = gettext('From`date`');
this.HAS_ERROR = 'has-error';
this.INVALID_DATE_TIME = 'invalidDateTime';
this.THIS_DATE_IS_INVALID = gettext('This date is invalid.');
this.THIS_DATE_IS_AFTER_THE_LAST_ALLOWED_DATE = gettext('This date is after the latest allowed date.');
this.THIS_DATE_IS_BEFORE_THE_EARLIEST_ALLOWED_DATE = gettext('This date is before the earliest allowed date.');
this.TO_DATE = gettext('To`date`');
this.parentContainer = inject(ControlContainer);
}
onDateFromChange(dateFrom) {
this.updatedDate.emit({
dateFrom
});
}
onDateToChange(dateTo) {
this.updatedDate.emit({
dateTo
});
}
get parentFormGroup() {
return this.parentContainer.control;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DateRangePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: DateRangePickerComponent, isStandalone: true, selector: "c8y-date-range-picker", inputs: { isEmittingDateChange: "isEmittingDateChange", showLabel: "showLabel" }, outputs: { updatedDate: "updatedDate" }, ngImport: i0, template: "<div class=\"d-flex gap-8 a-i-center flex-wrap\">\n <c8y-form-group\n [ngClass]=\"[\n parentFormGroup?.controls.dateFrom.errors ? HAS_ERROR : '',\n isEmittingDateChange ? 'd-flex a-i-center gap-4 m-b-0' : ''\n ]\"\n >\n <ng-container *ngIf=\"showLabel\">\n <label\n [title]=\"FROM_DATE | translate\"\n [for]=\"DATE_FROM\"\n >\n {{ FROM_DATE | translate }}\n </label>\n </ng-container>\n <c8y-date-time-picker\n id=\"DATE_FROM\"\n [maxDate]=\"parentFormGroup?.value.dateTo\"\n [placeholder]=\"FROM_DATE | translate\"\n [formControl]=\"parentFormGroup?.controls.dateFrom\"\n [ngClass]=\"parentFormGroup?.controls.dateFrom.errors ? HAS_ERROR : ''\"\n (ngModelChange)=\"onDateFromChange($event)\"\n ></c8y-date-time-picker>\n <c8y-messages\n class=\"text-nowrap\"\n [show]=\"parentFormGroup?.controls.dateFrom.errors\"\n >\n <c8y-message\n name=\"dateAfterRangeMax\"\n [text]=\"THIS_DATE_IS_AFTER_THE_LAST_ALLOWED_DATE | translate\"\n ></c8y-message>\n <c8y-message\n name=\"INVALID_DATE_TIME\"\n [text]=\"THIS_DATE_IS_INVALID | translate\"\n ></c8y-message>\n </c8y-messages>\n </c8y-form-group>\n <c8y-form-group\n [ngClass]=\"[\n parentFormGroup?.controls.dateTo.errors ? HAS_ERROR : '',\n isEmittingDateChange ? 'd-flex a-i-center gap-4 m-b-0' : ''\n ]\"\n >\n <ng-container *ngIf=\"showLabel\">\n <label\n [title]=\"TO_DATE | translate\"\n [for]=\"DATE_TO\"\n >\n {{ TO_DATE | translate }}\n </label>\n </ng-container>\n <c8y-date-time-picker\n id=\"DATE_TO\"\n [minDate]=\"parentFormGroup?.value.dateFrom\"\n [placeholder]=\"TO_DATE | translate\"\n [formControl]=\"parentFormGroup?.controls.dateTo\"\n [ngClass]=\"parentFormGroup?.controls.dateTo.errors ? HAS_ERROR : ''\"\n (ngModelChange)=\"onDateToChange($event)\"\n ></c8y-date-time-picker>\n <c8y-messages [show]=\"parentFormGroup?.controls.dateTo.errors\">\n <c8y-message\n name=\"dateBeforeRangeMin\"\n [text]=\"THIS_DATE_IS_BEFORE_THE_EARLIEST_ALLOWED_DATE | translate\"\n ></c8y-message>\n <c8y-message\n name=\"INVALID_DATE_TIME\"\n [text]=\"THIS_DATE_IS_INVALID | translate\"\n ></c8y-message>\n </c8y-messages>\n </c8y-form-group>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "pipe", type: i1$1.C8yTranslatePipe, name: "translate" }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "component", type: i1$1.FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "directive", type: i1$1.MessageDirective, selector: "c8y-message", inputs: ["name", "text"] }, { kind: "component", type: i1$1.MessagesComponent, selector: "c8y-messages", inputs: ["show", "defaults", "helpMessage"] }, { kind: "directive", type: i3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i1$1.DateTimePickerComponent, selector: "c8y-date-time-picker", inputs: ["minDate", "maxDate", "placeholder", "dateInputFormat", "adaptivePosition", "size", "dateType", "config"], outputs: ["onDateSelected"] }, { kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }], viewProviders: [
{
provide: ControlContainer,
useFactory: () => inject(ControlContainer, { skipSelf: true })
}
], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DateRangePickerComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-date-range-picker', standalone: true, imports: [CoreModule, CommonModule, ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.OnPush, viewProviders: [
{
provide: ControlContainer,
useFactory: () => inject(ControlContainer, { skipSelf: true })
}
], template: "<div class=\"d-flex gap-8 a-i-center flex-wrap\">\n <c8y-form-group\n [ngClass]=\"[\n parentFormGroup?.controls.dateFrom.errors ? HAS_ERROR : '',\n isEmittingDateChange ? 'd-flex a-i-center gap-4 m-b-0' : ''\n ]\"\n >\n <ng-container *ngIf=\"showLabel\">\n <label\n [title]=\"FROM_DATE | translate\"\n [for]=\"DATE_FROM\"\n >\n {{ FROM_DATE | translate }}\n </label>\n </ng-container>\n <c8y-date-time-picker\n id=\"DATE_FROM\"\n [maxDate]=\"parentFormGroup?.value.dateTo\"\n [placeholder]=\"FROM_DATE | translate\"\n [formControl]=\"parentFormGroup?.controls.dateFrom\"\n [ngClass]=\"parentFormGroup?.controls.dateFrom.errors ? HAS_ERROR : ''\"\n (ngModelChange)=\"onDateFromChange($event)\"\n ></c8y-date-time-picker>\n <c8y-messages\n class=\"text-nowrap\"\n [show]=\"parentFormGroup?.controls.dateFrom.errors\"\n >\n <c8y-message\n name=\"dateAfterRangeMax\"\n [text]=\"THIS_DATE_IS_AFTER_THE_LAST_ALLOWED_DATE | translate\"\n ></c8y-message>\n <c8y-message\n name=\"INVALID_DATE_TIME\"\n [text]=\"THIS_DATE_IS_INVALID | translate\"\n ></c8y-message>\n </c8y-messages>\n </c8y-form-group>\n <c8y-form-group\n [ngClass]=\"[\n parentFormGroup?.controls.dateTo.errors ? HAS_ERROR : '',\n isEmittingDateChange ? 'd-flex a-i-center gap-4 m-b-0' : ''\n ]\"\n >\n <ng-container *ngIf=\"showLabel\">\n <label\n [title]=\"TO_DATE | translate\"\n [for]=\"DATE_TO\"\n >\n {{ TO_DATE | translate }}\n </label>\n </ng-container>\n <c8y-date-time-picker\n id=\"DATE_TO\"\n [minDate]=\"parentFormGroup?.value.dateFrom\"\n [placeholder]=\"TO_DATE | translate\"\n [formControl]=\"parentFormGroup?.controls.dateTo\"\n [ngClass]=\"parentFormGroup?.controls.dateTo.errors ? HAS_ERROR : ''\"\n (ngModelChange)=\"onDateToChange($event)\"\n ></c8y-date-time-picker>\n <c8y-messages [show]=\"parentFormGroup?.controls.dateTo.errors\">\n <c8y-message\n name=\"dateBeforeRangeMin\"\n [text]=\"THIS_DATE_IS_BEFORE_THE_EARLIEST_ALLOWED_DATE | translate\"\n ></c8y-message>\n <c8y-message\n name=\"INVALID_DATE_TIME\"\n [text]=\"THIS_DATE_IS_INVALID | translate\"\n ></c8y-message>\n </c8y-messages>\n </c8y-form-group>\n</div>\n" }]
}], propDecorators: { isEmittingDateChange: [{
type: Input
}], showLabel: [{
type: Input
}], updatedDate: [{
type: Output
}] } });
function minOneDatapointActive() {
return (control) => {
const datapoints = control.value;
if (!datapoints || !datapoints.length) {
return null;
}
const activeDatapoints = datapoints.filter(datapoint => datapoint.__active);
if (activeDatapoints.length >= 1) {
return null;
}
return { exactlyOneDatapointNeedsToBeActive: true };
};
}
class DatapointsTableWidgetConfigComponent {
constructor(aggregationService, datapointsTableService, formBuilder, form, widgetConfig) {
this.aggregationService = aggregationService;
this.datapointsTableService = datapointsTableService;
this.formBuilder = formBuilder;
this.form = form;
this.widgetConfig = widgetConfig;
this.AGGREGATION_LABELS = AGGREGATION_LABELS;
this.DATE_SELECTION_LABELS = DATE_SELECTION_LABELS;
this.DEFAULT_DATE_SELECTOR_VALUE = DATE_SELECTION_VALUES.dashboard_context;
this.DEFAULT_INTERVAL_VALUE = INTERVAL_VALUES.hours;
this.TIME_RANGE_INTERVAL_LABELS = TIME_RANGE_INTERVAL_LABELS;
this.AGGREGATION_VALUES_ARR = AGGREGATION_VALUES_ARR;
this.DATE_SELECTION_VALUES_ARR = DATE_SELECTION_VALUES_ARR;
this.INTERVAL_VALUES_ARR = INTERVAL_VALUES_ARR;
this.REFRESH_INTERVAL_VALUES_ARR = REFRESH_INTERVAL_VALUES_ARR;
this.datapointSelectionConfig = {};
this.disabledAggregationOptions = {};
this.defaultFormOptions = {
selectableChartLineTypes: [],
selectableAxisTypes: [],
showRedRange: true,
showYellowRange: true
};
this.decimalLimits = {
numberOfDecimalPlacesMin: 0,
numberOfDecimalPlacesMax: 10
};
/**
* Indicate when the time interval selector item has been changed programmatically.
*
* This property is used to track changes in the time interval selector
* that are not triggered by direct user interaction, but by the application itself.
*
* In our case, the date selector and the interval selector are linked.
* So, when one of them changes, it affects the other.
* For example, selecting "Last hour" in the interval selector should automatically update the date range to the last hour.
* But without this flag, changing the date range would also update the interval selector, resulting in "Custom date" being selected.
* This happens because the system would interpret the behavior this way.
*/
this.isIntervalSelectorChangedProgrammatically = false;
this.destroy$ = new Subject();
}
ngOnInit() {
if (this.widgetConfig.context?.id) {
this.datapointSelectionConfig.contextAsset = this.widgetConfig?.context;
}
this.isWidgetLinkedToGlobalTimeContext =
this.config.widgetInstanceGlobalAutoRefreshContext ?? true;
this.initForm();
this.handleAutoRefreshToggleChanges();
this.handleIntervalSelectorChanges();
this.handleDateSelectorChanges();
this.handleGlobalDateSelectorChanges();
this.updateDisabledAggregationOptions();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
onBeforeSave(config) {
if (this.formGroup.value.aggregation === AGGREGATION_VALUES.none ||
(!this.formGroup.value.aggregation && config.aggregation === AGGREGATION_VALUES.none)) {
// The 'NONE' aggregation type is not handled by a backend, so it simply needs to be deleted to get data without aggregation.
this.deleteAggregationProperty(config);
}
this.updateTimeContext(config, this.formGroup.value.globalDateSelector);
Object.assign(config, this.formGroup.value);
}
toggleRefreshIntervalControl() {
this.formGroup.controls.isAutoRefreshEnabled.value
? this.formGroup.controls.refreshInterval.disable()
: this.formGroup.controls.refreshInterval.enable();
}
updateTimeContext(config, selectedDateContext) {
switch (selectedDateContext) {
case DATE_SELECTION_VALUES.dashboard_context:
config.displayDateSelection = false;
config.widgetInstanceGlobalAutoRefreshContext = true;
if (config.aggregation !== 'NONE') {
this.deleteAggregationProperty(config);
}
break;
case DATE_SELECTION_VALUES.view_and_config:
config.displayDateSelection = true;
config.widgetInstanceGlobalAutoRefreshContext = false;
break;
case DATE_SELECTION_VALUES.config:
default:
config.displayDateSelection = false;
config.widgetInstanceGlobalAutoRefreshContext = false;
}
}
initForm() {
this.formGroup = this.createForm();
// When the 'NONE' aggregation is removed at the 'onBeforeSave' block,
// and a Global Date Context forces widgets auto refresh to be enabled
// then, the aggregation selector will be empty on initial load.
this.config.aggregation = this.config.aggregation ?? AGGREGATION_VALUES.none;
this.form.form.addControl('config', this.formGroup);
this.formGroup.patchValue(this.config);
}
createForm() {
const { dateFrom, dateTo } = this.datapointsTableService.calculateDateRange(this.DEFAULT_INTERVAL_VALUE);
const isLegacyWidget = this.config.hasOwnProperty('widgetInstanceGlobalTimeContext') &&
!this.config.hasOwnProperty('decimalPlaces');
return this.formBuilder.group({
aggregation: new FormControl({
value: AGGREGATION_VALUES.none,
disabled: !this.isAutoRefershDisabled()
}),
datapoints: this.formBuilder.control(new Array(), [
Validators.required,
Validators.minLength(1),
minOneDatapointActive()
]),
globalDateSelector: new FormControl(isLegacyWidget
? this.determineGlobalDateSelectorValueForLegacyWidget()
: this.DEFAULT_DATE_SELECTOR_VALUE),
dateFrom: new FormControl(dateFrom),
dateTo: new FormControl(dateTo),
decimalPlaces: [
2,
[
Validators.required,
Validators.min(this.decimalLimits.numberOfDecimalPlacesMin),
Validators.max(this.decimalLimits.numberOfDecimalPlacesMax),
Validators.pattern('^[0-9]+$')
]
],
interval: new FormControl(this.DEFAULT_INTERVAL_VALUE),
isAutoRefreshEnabled: isLegacyWidget ? this.config.realtime : [true],
refreshInterval: new FormControl({
value: DEFAULT_DPT_REFRESH_INTERVAL_VALUE,
disabled: this.isAutoRefershDisabled()
})
}, { validators: dateRangeValidator });
}
determineGlobalDateSelectorValueForLegacyWidget() {
const { displayDateSelection, widgetInstanceGlobalAutoRefreshContext } = this.config;
// State of widgetInstanceGlobalTimeContext was converted to widgetInstanceGlobalAutoRefreshContext.
// That is why it use here in a legacy widget context.
if (widgetInstanceGlobalAutoRefreshContext) {
return DATE_SELECTION_VALUES.dashboard_context;
}
return displayDateSelection
? DATE_SELECTION_VALUES.view_and_config
: DATE_SELECTION_VALUES.config;
}
isAutoRefershDisabled() {
const isInitialWidgetCreation = Object.keys(this.config).length === 1 && this.config['settings'];
if (isInitialWidgetCreation) {
return false;
}
const isLegacyWidgetRealtimeNotActive = !this.config.realtime &&
!this.config.hasOwnProperty('decimalPlaces') &&
!this.config.hasOwnProperty('refreshInterval') &&
!this.config.hasOwnProperty('isAutoRefreshEnabled');
if (isLegacyWidgetRealtimeNotActive) {
return true;
}
return !this.config.isAutoRefreshEnabled;
}
/**
* Handles changes to the auto-refresh toggle control and updates the aggregation control accordingly.
*
* This method subscribes to the value changes of the auto-refresh toggle form control.
* When auto-refresh is enabled, the aggregation control's value is set to 'none' and the control is
* visually disabled (but not programmatically disabled to ensure the value is saved).
* When auto-refresh is disabled, the aggregation control is enabled.
*/
handleAutoRefreshToggleChanges() {
this.formGroup.controls.isAutoRefreshEnabled.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((isAutoRefreshEnabled) => {
const aggregationControl = this.formGroup.controls.aggregation;
if (isAutoRefreshEnabled) {
aggregationControl.setValue(AGGREGATION_VALUES.none);
// Do not disable the control programmatically to ensure the value is saved.
}
else {
aggregationControl.enable();
}
});
}
/**
* Handles changes in the interval selector form control.
*/
handleIntervalSelectorChanges() {
this.formGroup.controls.interval.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((selectedInterval) => {
if (selectedInterval === INTERVAL_VALUES.custom) {
this.updateDisabledAggregationOptions();
return;
}
this.handleNonCustomInterval(selectedInterval);
});
}
handleNonCustomInterval(selectedInterval) {
this.isIntervalSelectorChangedProgrammatically = true;
this.updateDateRange(selectedInterval);
this.updateDisabledAggregationOptions();
this.updateAggregationIfAutoRefreshDisabled(selectedInterval);
}
updateDateRange(selectedInterval) {
const dateRange = this.datapointsTableService.calculateDateRange(selectedInterval);
this.formGroup.controls.dateTo.setValue(dateRange.dateTo);
// MTM-61351
// Use requestAnimationFrame to queue the dateFrom update.
// This prevents timing issues where rapid setValue calls might
// cause the view to go out of sync with form control values,
// especially during the first change after initialization.
// requestAnimationFrame(() => {
/**
* Without this requestAnimationFrame or setTimeout, dateFrom won't change to a correct value after first selector change.
* When form is saved it saves with a correct value, even without with this fix.
* How to reproduce:
* 1. set date values to e.g.: 01.05.2024-30.05.2024 and save it
* 2. reopen a config and switch interval to last month
*/
// This fix brakes a logic behind disabling non available aggregations.
// Form will be still saved with correct date value, only view is out of a sync.
this.formGroup.controls.dateFrom.setValue(dateRange.dateFrom);
this.isIntervalSelectorChangedProgrammatically = false;
// });
}
updateDisabledAggregationOptions() {
this.disabledAggregationOptions = this.aggregationService.getDisabledAggregationOptions(this.formGroup.controls.dateFrom.value, this.formGroup.controls.dateTo.value);
}
updateAggregationIfAutoRefreshDisabled(selectedInterval) {
const isAutoRefreshDisabled = !this.formGroup.controls.isAutoRefreshEnabled.value;
if (isAutoRefreshDisabled) {
this.setAggregationValue(selectedInterval);
}
}
setAggregationValue(interval) {
const aggregationControl = this.formGroup.controls.aggregation;
const newAggregationValue = this.aggregationService.determineAggregationValue(interval);
aggregationControl.setValue(newAggregationValue);
}
/**
* Handles changes in the date selector form control.
*/
handleDateSelectorChanges() {
merge(this.formGroup.controls.dateFrom.valueChanges, this.formGroup.controls.dateTo.valueChanges)
.pipe(takeUntil(this.destroy$))
.subscribe(() => this.handleDateChange());
}
handleDateChange() {
if (this.isIntervalSelectorChangedProgrammatically) {
return;
}
const intervalControl = this.formGroup.controls.interval;
if (intervalControl.value === INTERVAL_VALUES.custom) {
this.handleCustomIntervalDateChange();
}
else {
this.setIntervalToCustom(intervalControl);
}
this.setToFirstAvailableAggregationOptionIfCurrentAggregationIsDisabled();
}
handleCustomIntervalDateChange() {
this.updateDisabledAggregationOptions();
}
setIntervalToCustom(intervalControl) {
intervalControl.setValue(INTERVAL_VALUES.custom);
}
/**
* Handles changes in the global date selector form control.
*/
handleGlobalDateSelectorChanges() {
this.formGroup.controls.globalDateSelector.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((selected) => {
if (selected === DATE_SELECTION_VALUES.dashboard_context) {
this.handleDashboardContext();
}
else {
this.handleNonDashboardContext();
}
});
}
handleDashboardContext() {
this.isWidgetLinkedToGlobalTimeContext = true;
}
handleNonDashboardContext() {
this.isWidgetLinkedToGlobalTimeContext = false;
if (this.isConfigSavedWithoutDashboardTimeOption()) {
return;
}
this.updateAggregationIfNecessary();
this.setIntervalToCustom(this.formGroup.controls.interval);
}
isConfigSavedWithoutDashboardTimeOption() {
return !this.config.widgetInstanceGlobalAutoRefreshContext;
}
updateAggregationIfNecessary() {
const aggregationControl = this.formGroup.controls.aggregation;
const isDashboardTimeOptionSetAggregationToNull = !aggregationControl.value;
if (isDashboardTimeOptionSetAggregationToNull) {
aggregationControl.setValue(AGGREGATION_VALUES.none);
}
}
/**
* Sets the aggregation control to the first available (non-disabled) option if the current option is disabled.
*
* This method:
* - Retrieves the current value of the aggregation control.
* - Checks if the current aggregation option is disabled.
* - If the current option is disabled, sets the control to the first available (non-disabled) option based on the following order:
* - If the current value is `DAILY`, it switches to `HOURLY` if it's not disabled, otherwise to `MINUTELY` if `HOURLY` is also disabled.
* - If the current value is `HOURLY`, it switches to `MINUTELY` if it's not disabled.
* - If all options are disabled, it sets the value to `NONE`.
*
* The disabled state is stored in the `disabledAggregationOptions` object,
* where the key is the aggregation value and the value is a boolean indicating whether the option is disabled.
*
* The `AGGREGATION_VALUES` object defines the possible aggregation values.
*/
setToFirstAvailableAggregationOptionIfCurrentAggregationIsDisabled() {
const aggregationControl = this.formGroup.controls.aggregation;
const currentValue = aggregationControl.value;
const newAggregationValue = this.aggregationService.determineFirstNewAvailableAggregationValue(currentValue, this.disabledAggregationOptions);
if (newAggregationValue !== currentValue) {
aggregationControl.setValue(newAggregationValue);
}
}
deleteAggregationProperty(config) {
delete this.formGroup.value.aggregation;
delete config.aggregation;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DatapointsTableWidgetConfigComponent, deps: [{ token: i1$1.AggregationService }, { token: DatapointsTableService }, { token: i3.FormBuilder }, { token: i3.NgForm }, { token: i4.WidgetConfigComponent }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: DatapointsTableWidgetConfigComponent, isStandalone: true, selector: "c8y-datapoints-table-view-config", inputs: { config: "config" }, ngImport: i0, template: "<div class=\"p-l-24 p-r-24\">\n <form\n class=\"no-card-context\"\n [formGroup]=\"formGroup\"\n >\n <!-- <datapoints-selector> -->\n <div class=\"col-md-6\">\n <div class=\"row\">\n <!-- global-time-context-selector -->\n <div class=\"form-group m-b-0 p-b-16 separator-bottom\">\n <label\n class=\"d-flex a-i-center p-t-4\"\n for=\"dateSelection\"\n >\n {{ 'Date selection' | translate }}\n <button\n class=\"btn-help btn-help--sm\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"popoverTemplate\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n [adaptivePosition]=\"true\"\n ></button>\n </label>\n <ng-template #popoverTemplate>\n <span translate>\n Choose how to select a date range, the available options are:\n <ul class=\"m-l-0 p-l-8 m-t-8 m-b-0\">\n <li>\n <b>Dashboard time range:</b>\n restricts date selection to the global dashboard configuration only\n </li>\n <li>\n <b>Widget configuration:</b>\n restricts the date selection only to the widget configuration\n </li>\n <li>\n <b>Widget and widget configuration:</b>\n restricts the date selection to the widget view and widget configuration only\n </li>\n </ul>\n </span>\n </ng-template>\n <div class=\"c8y-select-wrapper\">\n <select\n class=\"form-control text-12\"\n [title]=\"'Date selection' | translate\"\n [attr.aria-label]=\"'Date selection' | translate\"\n id=\"globalDateSelector\"\n formControlName=\"globalDateSelector\"\n >\n <option\n *ngFor=\"let dataSelectionValue of DATE_SELECTION_VALUES_ARR\"\n [ngValue]=\"dataSelectionValue\"\n >\n {{ DATE_SELECTION_LABELS[dataSelectionValue] | translate }}\n </option>\n </select>\n </div>\n </div>\n </div>\n <div class=\"row\">\n <c8y-datapoint-selection-list\n class=\"bg-inherit separator-top p-t-16 d-block\"\n listTitle=\"{{ 'Data points' | translate }}\"\n name=\"datapoints\"\n [defaultFormOptions]=\"defaultFormOptions\"\n [config]=\"datapointSelectionConfig\"\n [minActiveCount]=\"1\"\n formControlName=\"datapoints\"\n ></c8y-datapoint-selection-list>\n </div>\n </div>\n <div class=\"col-md-6\">\n <div class=\"row\">\n <ng-container *ngIf=\"!isWidgetLinkedToGlobalTimeContext\">\n <div class=\"col-md-6\">\n <!-- interval selector -->\n <fieldset class=\"c8y-fieldset\">\n <legend class=\"d-flex a-i-center\">\n {{ 'Auto refresh' | translate }}\n <button\n class=\"btn-help btn-help--sm\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"\n 'Change the state of interval automatic refresh and set the refresh frequency.'\n | translate\n \"\n placement=\"top\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n [adaptivePosition]=\"true\"\n ></button>\n </legend>\n <c8y-form-group class=\"m-b-16 form-group-sm\">\n <div class=\"d-flex gap-4 m-t-8 m-b-8 a-i-center\">\n <label class=\"c8y-switch\">\n <input\n id=\"refreshToggle\"\n name=\"isAutoRefreshEnabled\"\n type=\"checkbox\"\n formControlName=\"isAutoRefreshEnabled\"\n (click)=\"toggleRefreshIntervalControl()\"\n />\n <span></span>\n <span class=\"sr-only\">{{ 'Auto refresh' | translate }}</span>\n </label>\n <label\n class=\"m-b-0\"\n for=\"refreshInterval\"\n >\n {{ 'Interval' | translate }}\n </label>\n <div class=\"c8y-select-wrapper\">\n <select\n class=\"form-control text-12\"\n [title]=\"'Refresh interval in seconds' | translate\"\n id=\"refreshInterval\"\n formControlName=\"refreshInterval\"\n >\n <option\n *ngFor=\"let refreshInterval of REFRESH_INTERVAL_VALUES_ARR\"\n [ngValue]=\"refreshInterval\"\n >\n {{ '{{ seconds }} s' | translate: { seconds: refreshInterval / 1000 } }}\n </option>\n </select>\n </div>\n </div>\n </c8y-form-group>\n </fieldset>\n </div>\n </ng-container>\n <!-- decimal input -->\n <div class=\"col-md-6\">\n <fieldset class=\"c8y-fieldset\">\n <legend>\n {{ 'Decimal places' | translate }}\n </legend>\n <c8y-form-group class=\"p-t-8\">\n <input\n class=\"form-control\"\n name=\"decimalPlaces\"\n type=\"number\"\n formControlName=\"decimalPlaces\"\n step=\"1\"\n />\n </c8y-form-group>\n </fieldset>\n </div>\n </div>\n <!-- aggregation selector -->\n <div class=\"row\">\n <ng-container *ngIf=\"!isWidgetLinkedToGlobalTimeContext\">\n <div class=\"col-md-6\">\n <fieldset class=\"c8y-fieldset\">\n <legend>\n {{ 'Aggregation' | translate }}\n </legend>\n <c8y-form-group class=\"p-t-8\">\n <div class=\"c8y-select-wrapper\">\n <!-- Setting below [attr.disabled] ensures that the control is visually disabled and user interaction is prevented,\n while still allowing the value to be updated and saved correctly in the form.\n This solution covers the case where the user enables auto-refresh, in which case aggregation must be set to NONE. -->\n <select\n class=\"form-control text-12\"\n [title]=\"'Aggregation' | translate\"\n id=\"aggregation\"\n formControlName=\"aggregation\"\n [attr.disabled]=\"formGroup.value.isAutoRefreshEnabled ? true : null\"\n >\n <option\n *ngFor=\"let aggregationValue of AGGREGATION_VALUES_ARR\"\n [ngValue]=\"aggregationValue\"\n [disabled]=\"disabledAggregationOptions[aggregationValue]\"\n >\n {{ AGGREGATION_LABELS[aggregationValue] | translate }}\n </option>\n </select>\n </div>\n </c8y-form-group>\n </fieldset>\n </div>\n </ng-container>\n <!-- time interval selector -->\n <div class=\"col-md-6\">\n <fieldset class=\"c8y-fieldset\">\n <legend>\n {{ 'Time interval' | translate }}\n </legend>\n <c8y-form-group class=\"p-t-8\">\n <div class=\"c8y-select-wrapper\">\n <select\n class=\"form-control text-12\"\n [title]=\"'Interval' | translate\"\n id=\"interval\"\n formControlName=\"interval\"\n >\n <option\n *ngFor=\"let intervalValue of INTERVAL_VALUES_ARR\"\n [ngValue]=\"intervalValue\"\n >\n {{ TIME_RANGE_INTERVAL_LABELS[intervalValue] | translate }}\n </option>\n </select>\n </div>\n </c8y-form-group>\n </fieldset>\n </div>\n </div>\n <!-- date pickers -->\n <fieldset class=\"c8y-fieldset\">\n <legend>\n {{ 'Date range' | translate }}\n </legend>\n <c8y-date-range-picker></c8y-date-range-picker>\n </fieldset>\n </div>\n </form>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "pipe", type: i1$1.C8yTranslatePipe, name: "translate" }, { kind: "directive", type: i1$1.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { 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: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i3.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i3.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "component", type: i1$1.FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "directive", type: i1$1.RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { kind: "directive", type: i3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i3.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: DatapointSelectorModule }, { kind: "component", type: i6.DatapointSelectionListComponent, selector: "c8y-datapoint-selection-list", inputs: ["actions", "allowDragAndDrop", "config", "defaultFormOptions", "maxActiveCount", "minActiveCount", "resolveContext", "listTitle"], outputs: ["isValid", "change"] }, { kind: "component", type: DateRangePickerComponent, selector: "c8y-date-range-picker", inputs: ["isEmittingDateChange", "showLabel"], outputs: ["updatedDate"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "directive", type: i7.PopoverDirective, selector: "[popover]", inputs: ["adaptivePosition", "boundariesElement", "popover", "popoverContext", "popoverTitle", "placement", "outsideClick", "triggers", "container", "containerClass", "isOpen", "delay"], outputs: ["onShown", "onHidden"], exportAs: ["bs-popover"] }, { kind: "ngmodule", type: ReactiveFormsModule }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DatapointsTableWidgetConfigComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-datapoints-table-view-config', standalone: true, imports: [
CoreModule,
DatapointSelectorModule,
DateRangePickerComponent,
PopoverModule,
ReactiveFormsModule
], template: "<div class=\"p-l-24 p-r-24\">\n <form\n class=\"no-card-context\"\n [formGroup]=\"formGroup\"\n >\n <!-- <datapoints-selector> -->\n <div class=\"col-md-6\">\n <div class=\"row\">\n <!-- global-time-context-selector -->\n <div class=\"form-group m-b-0 p-b-16 separator-bottom\">\n <label\n class=\"d-flex a-i-center p-t-4\"\n for=\"dateSelection\"\n >\n {{ 'Date selection' | translate }}\n <button\n class=\"btn-help btn-help--sm\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"popoverTemplate\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n [adaptivePosition]=\"true\"\n ></button>\n </label>\n <ng-template #popoverTemplate>\n <span translate>\n Choose how to select a date range, the available options are:\n <ul class=\"m-l-0 p-l-8 m-t-8 m-b-0\">\n <li>\n <b>Dashboard time range:</b>\n restricts date selection to the global dashboard configuration only\n </li>\n <li>\n <b>Widget configuration:</b>\n restricts the date selection only to the widget configuration\n </li>\n <li>\n <b>Widget and widget configuration:</b>\n restricts the date selection to the widget view and widget configuration only\n </li>\n </ul>\n </span>\n </ng-template>\n <div class=\"c8y-select-wrapper\">\n <select\n class=\"form-control text-12\"\n [title]=\"'Date selection' | translate\"\n [attr.aria-label]=\"'Date selection' | translate\"\n id=\"globalDateSelector\"\n formControlName=\"globalDateSelector\"\n >\n <option\n *ngFor=\"let dataSelectionValue of DATE_SELECTION_VALUES_ARR\"\n [ngValue]=\"dataSelectionValue\"\n >\n {{ DATE_SELECTION_LABELS[dataSelectionValue] | translate }}\n </option>\n </select>\n </div>\n </div>\n </div>\n <div class=\"row\">\n <c8y-datapoint-selection-list\n class=\"bg-inherit separator-top p-t-16 d-block\"\n listTitle=\"{{ 'Data points' | translate }}\"\n name=\"datapoints\"\n [defaultFormOptions]=\"defaultFormOptions\"\n [config]=\"datapointSelectionConfig\"\n [minActiveCount]=\"1\"\n formControlName=\"datapoints\"\n ></c8y-datapoint-selection-list>\n </div>\n </div>\n <div class=\"col-md-6\">\n <div class=\"row\">\n <ng-container *ngIf=\"!isWidgetLinkedToGlobalTimeContext\">\n <div class=\"col-md-6\">\n <!-- interval selector -->\n <fieldset class=\"c8y-fieldset\">\n <legend class=\"d-flex a-i-center\">\n {{ 'Auto refresh' | translate }}\n <button\n class=\"btn-help btn-help--sm\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"\n 'Change the state of interval automatic refresh and set the refresh frequency.'\n | translate\n \"\n placement=\"top\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n [adaptivePosition]=\"true\"\n ></button>\n </legend>\n <c8y-form-group class=\"m-b-16 form-group-sm\">\n <div class=\"d-flex gap-4 m-t-8 m-b-8 a-i-center\">\n <label class=\"c8y-switch\">\n <input\n id=\"refreshToggle\"\n name=\"isAutoRefreshEnabled\"\n type=\"checkbox\"\n formControlName=\"isAutoRefreshEnabled\"\n (click)=\"toggleRefreshIntervalControl()\"\n />\n <span></span>\n <span class=\"sr-only\">{{ 'Auto refresh' | translate }}</span>\n </label>\n <label\n class=\"m-b-0\"\n for=\"refreshInterval\"\n >\n {{ 'Interval' | translate }}\n </label>\n <div class=\"c8y-select-wrapper\">\n <select\n class=\"form-control text-12\"\n [title]=\"'Refresh interval in seconds' | translate\"\n id=\"refreshInterval\"\n formControlName=\"refreshInterval\"\n >\n <option\n *ngFor=\"let refreshInterval of REFRESH_INTERVAL_VALUES_ARR\"\n [ngValue]=\"refreshInterval\"\n >\n {{ '{{ seconds }} s' | translate: { seconds: refreshInter