UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

506 lines (504 loc) 97.8 kB
import * as i0 from '@angular/core'; import { signal, inject, computed, ViewChild, Input, Component, Optional } from '@angular/core'; import * as i2 from '@angular/forms'; import { FormBuilder, NgForm, Validators } from '@angular/forms'; import * as i1 from '@c8y/ngx-components'; import { GainsightService, CommonModule, CoreModule, FormsModule, WidgetTimeContextDateRangeService, AGGREGATION_ICONS, AGGREGATION_TEXTS, DashboardChildComponent, WidgetActionWrapperComponent, SelectComponent, SelectItemDirective, SelectedItemsDirective } from '@c8y/ngx-components'; import { AlarmEventSelectorModule } from '@c8y/ngx-components/alarm-event-selector'; import * as i2$1 from '@c8y/ngx-components/context-dashboard'; import { WidgetConfigService } from '@c8y/ngx-components/context-dashboard'; import { DatapointSelectorModule } from '@c8y/ngx-components/datapoint-selector'; import { ChartHelpersService, CHART_VIEW_CONTEXT, LEGEND_DISPLAY_OPTIONS, PRODUCT_EXPERIENCE_DATA_EXPLORER_AND_GRAPH, CHART_DISPLAY_OPTION_DEFAULTS, ChartsComponent, ChartEventsService, ChartAlarmsService } from '@c8y/ngx-components/echart'; import * as i3$1 from '@c8y/ngx-components/global-context'; import { PRESET_NAME, LocalControlsComponent, GLOBAL_CONTEXT_DISPLAY_MODE, GLOBAL_CONTEXT_SOURCE, GLOBAL_CONTEXT_EVENTS, REFRESH_OPTION, GlobalContextConnectorComponent } from '@c8y/ngx-components/global-context'; import * as i4 from 'ngx-bootstrap/popover'; import { PopoverModule } from 'ngx-bootstrap/popover'; import * as i3 from 'ngx-bootstrap/tooltip'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; import { Subject, takeUntil, BehaviorSubject } from 'rxjs'; import { A11yModule } from '@angular/cdk/a11y'; import * as i5 from '@angular/common'; import { CommonModule as CommonModule$1 } from '@angular/common'; import { ALARM_STATUS_LABELS } from '@c8y/client'; import * as i9 from '@c8y/ngx-components/alarms'; import { AlarmsModule } from '@c8y/ngx-components/alarms'; import { DatapointsExportSelectorComponent } from '@c8y/ngx-components/datapoints-export-selector'; import { gettext } from '@c8y/ngx-components/gettext'; import * as i1$1 from '@ngx-translate/core'; import { merge, cloneDeep } from 'lodash-es'; import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; class DatapointsGraphWidgetConfigComponent { set config(value) { if (!value) return; if (this.isFirstConfigSet) { this._config.set({ ...value }); this.isFirstConfigSet = false; } else { this._config.set({ ...this._config(), dateFrom: value.dateFrom, dateTo: value.dateTo, interval: value.interval, refreshInterval: value.refreshInterval, isAutoRefreshEnabled: value.isAutoRefreshEnabled, aggregation: value.aggregation, realtime: value.realtime, displayMode: value.displayMode, refreshOption: value.refreshOption, dateTimeContext: value.dateTimeContext, datapoints: value.datapoints, alarmsEventsConfigs: value.alarmsEventsConfigs, showAdvancedChartOptions: value.showAdvancedChartOptions }); } } get config() { return this._config(); } set previewMapSet(template) { if (template) { this.widgetConfigService.setPreview(template); return; } this.widgetConfigService.setPreview(null); } constructor() { this._config = signal({}, ...(ngDevMode ? [{ debugName: "_config" }] : [])); this.isFirstConfigSet = true; this.formBuilder = inject(FormBuilder); this.form = inject(NgForm); this.widgetConfigService = inject(WidgetConfigService); this.chartHelpersService = inject(ChartHelpersService); this.gainsightService = inject(GainsightService); this.datapointSelectionConfig = {}; this.activeDatapointsExists = false; this.alarmsOrEventsHaveNoMatchingDps = false; this.chartViewContext = CHART_VIEW_CONTEXT.WIDGET_CONFIG; this.legendDisplayOptions = LEGEND_DISPLAY_OPTIONS; this.widgetControls = PRESET_NAME.DATAPOINTS_GRAPH; this.previewContextConfig = computed(() => { const cfg = this._config(); return { dateTimeContext: cfg.dateTimeContext, aggregation: cfg.aggregation, isAutoRefreshEnabled: cfg.isAutoRefreshEnabled, refreshInterval: cfg.refreshInterval, refreshOption: cfg.refreshOption }; }, ...(ngDevMode ? [{ debugName: "previewContextConfig" }] : [])); this.destroy$ = new Subject(); this.isInitialized = false; this.formGroup = this.initForm(); } async ngOnInit() { const currentConfig = this._config(); this.form.form.addControl('config', this.formGroup); this.formGroup.patchValue({ ...currentConfig }, { emitEvent: false }); this.applyFormValuesToConfig(); this.isInitialized = true; this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => { this.applyFormValuesToConfig(); this.setActiveDatapointsExists(); this.checkForMatchingDatapoints(); }); } ngOnChanges(changes) { if (!changes.config || changes.config.isFirstChange() || !this.isInitialized) return; this.setActiveDatapointsExists(); this.checkForMatchingDatapoints(); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } onBeforeSave(config) { if (!this.formGroup.valid || !config) return false; const snapshot = { ...this._config() }; for (const key in snapshot) { if (snapshot.hasOwnProperty(key)) { try { config[key] = snapshot[key]; } catch (e) { // do nothing } } } this.widgetConfigService.updateConfig(snapshot, true); const configSummaryGS = this.chartHelpersService.getConfigSummaryForGainsight(config); this.gainsightService.triggerEvent(PRODUCT_EXPERIENCE_DATA_EXPLORER_AND_GRAPH.EVENTS.DATA_EXPLORER_AND_GRAPH, { component: PRODUCT_EXPERIENCE_DATA_EXPLORER_AND_GRAPH.COMPONENTS.DATA_EXPLORER_DETAILS, action: PRODUCT_EXPERIENCE_DATA_EXPLORER_AND_GRAPH.ACTIONS.DATA_GRAPH_WIDGET_CONFIG, ...configSummaryGS }); return true; } updateTimeRangeOnRealtime(timeRange) { const current = this._config(); this._config.set({ ...current, ...timeRange, interval: current.interval || 'hours' }); } updateDashboardTimeContext(timeProps) { const dateTimeContext = { dateFrom: new Date(timeProps.dateFrom).toISOString(), dateTo: new Date(timeProps.dateTo).toISOString(), interval: timeProps.interval }; this._config.set({ ...this._config(), dateTimeContext }); } updateAggregatedSliderDatapoint(selectedDatapoint) { const aggregatedDatapoint = this.chartHelpersService.findMatchingDatapoint(this._config().datapoints || [], selectedDatapoint); this._config.set({ ...this._config(), aggregatedDatapoint }); } applyFormValuesToConfig() { const formValue = this.formGroup.value; this._config.set({ ...this._config(), dataPointLegendDisplay: formValue.dataPointLegendDisplay, displayMarkedLine: formValue.displayMarkedLine, displayMarkedPoint: formValue.displayMarkedPoint, mergeMatchingDatapoints: formValue.mergeMatchingDatapoints, forceMergeDatapoints: formValue.forceMergeDatapoints, setYaxisStartToZero: formValue.setYaxisStartToZero, smoothLines: formValue.smoothLines, showLabelAndUnit: formValue.showLabelAndUnit, showSlider: formValue.showSlider, yAxisSplitLines: formValue.yAxisSplitLines, xAxisSplitLines: formValue.xAxisSplitLines, numberOfDecimalPlaces: formValue.numberOfDecimalPlaces, aggregatedDatapoint: formValue.aggregatedDatapoint }); } checkForMatchingDatapoints() { const config = this._config(); const alarmsEventsConfigs = config.alarmsEventsConfigs || []; const datapoints = config.datapoints || []; const allMatch = alarmsEventsConfigs.every(ae => datapoints.some(dp => dp.__target?.id === ae.__target?.id)); queueMicrotask(() => { this.alarmsOrEventsHaveNoMatchingDps = !allMatch; }); } initForm() { const initialState = this._config(); const chartDefaults = CHART_DISPLAY_OPTION_DEFAULTS; const form = this.formBuilder.group({ dataPointLegendDisplay: ['auto', []], displayMarkedLine: [chartDefaults.displayMarkedLine, []], displayMarkedPoint: [chartDefaults.displayMarkedPoint, []], mergeMatchingDatapoints: [chartDefaults.mergeMatchingDatapoints, []], forceMergeDatapoints: [chartDefaults.forceMergeDatapoints, []], setYaxisStartToZero: [chartDefaults.setYaxisStartToZero, []], smoothLines: [chartDefaults.smoothLines, []], showLabelAndUnit: [chartDefaults.showLabelAndUnit, []], displayAggregationSelection: [false, []], canDecoupleGlobalTimeContext: [false, []], showSlider: [chartDefaults.showSlider, [Validators.required]], yAxisSplitLines: [false, [Validators.required]], xAxisSplitLines: [false, [Validators.required]], numberOfDecimalPlaces: [ chartDefaults.numberOfDecimalPlaces, [Validators.required, Validators.min(0), Validators.max(10)] ], aggregatedDatapoint: [initialState?.aggregatedDatapoint || null] }); return form; } setActiveDatapointsExists() { const datapoints = this._config().datapoints || []; this.activeDatapointsExists = datapoints.filter(dp => dp.__active).length > 0; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DatapointsGraphWidgetConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: DatapointsGraphWidgetConfigComponent, isStandalone: true, selector: "c8y-datapoints-graph-widget-config", inputs: { config: "config" }, host: { classAttribute: "d-contents" }, providers: [ ChartEventsService, ChartAlarmsService, ChartHelpersService, WidgetTimeContextDateRangeService ], viewQueries: [{ propertyName: "previewMapSet", first: true, predicate: ["dataPointsGraphPreview"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<form [formGroup]=\"formGroup\">\n <div class=\"form-group form-group-sm\">\n <label\n [title]=\"'Number of decimal places' | translate\"\n translate\n >\n Number of decimal places\n </label>\n <input\n class=\"form-control\"\n name=\"numberOfDecimalPlaces\"\n type=\"number\"\n formControlName=\"numberOfDecimalPlaces\"\n [placeholder]=\"'e.g. {{ example }}' | translate: { example: 1 }\"\n />\n <c8y-messages\n [show]=\"\n formGroup.controls?.numberOfDecimalPlaces?.touched &&\n formGroup?.controls?.numberOfDecimalPlaces?.errors\n \"\n ></c8y-messages>\n </div>\n <c8y-form-group class=\"form-group-sm\">\n <label>\n {{ 'Data point legend display' | translate }}\n </label>\n <div\n class=\"c8y-select-wrapper\"\n [formGroup]=\"formGroup\"\n >\n <select\n class=\"form-control\"\n [title]=\"'Data point legend display' | translate\"\n id=\"dataPointLegendDisplay\"\n formControlName=\"dataPointLegendDisplay\"\n >\n @for (option of legendDisplayOptions; track option) {\n <option [ngValue]=\"option.value\">\n {{ option.label | translate }}\n </option>\n }\n </select>\n </div>\n </c8y-form-group>\n</form>\n\n<form\n class=\"d-block p-t-8\"\n [formGroup]=\"formGroup\"\n>\n <label>{{ 'Display options' | translate }}</label>\n <fieldset class=\"c8y-fieldset m-b-24 m-t-0\">\n <legend>{{ 'Axis' | translate }}</legend>\n <c8y-form-group class=\"p-b-16 m-b-0 p-t-8 form-group-sm\">\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Y-axis helper lines' | translate\"\n >\n <input\n name=\"yAxisSplitLines\"\n type=\"checkbox\"\n formControlName=\"yAxisSplitLines\"\n />\n <span></span>\n <span translate>Y-axis helper lines</span>\n </label>\n <label\n class=\"c8y-checkbox\"\n [title]=\"'X-axis helper lines' | translate\"\n >\n <input\n name=\"xAxisSplitLines\"\n type=\"checkbox\"\n formControlName=\"xAxisSplitLines\"\n />\n <span></span>\n <span translate>X-axis helper lines</span>\n </label>\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Merge matching data points into single axis' | translate\"\n >\n <input\n name=\"mergeMatchingDatapoints\"\n type=\"checkbox\"\n formControlName=\"mergeMatchingDatapoints\"\n />\n <span></span>\n <span translate>Merge matching data points into single axis</span>\n <button\n class=\"btn-help\"\n [attr.aria-label]=\"\n 'Data points with the same min and max values will be merged into a single axis. The values must be defined in the data point configuration.'\n | translate\n \"\n [popover]=\"\n 'Data points with the same min and max values will be merged into a single axis. The values must be defined in the data point configuration.'\n | translate\n \"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n (click)=\"$event.stopPropagation()\"\n [adaptivePosition]=\"false\"\n ></button>\n </label>\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Force merge all data points into single axis' | translate\"\n >\n <input\n name=\"forceMergeDatapoints\"\n type=\"checkbox\"\n formControlName=\"forceMergeDatapoints\"\n />\n <span></span>\n <span translate>Force merge all data points into a single axis</span>\n <button\n class=\"btn-help\"\n [attr.aria-label]=\"\n 'All axes will be force merged to a single axis with the scale being set to the max and min value of all axes. It\\'s recommended to use this option for data points with similar values.'\n | translate\n \"\n [popover]=\"\n 'All axes will be force merged to a single axis with the scale being set to the max and min value of all axes. It\\'s recommended to use this option for data points with similar values.'\n | translate\n \"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n (click)=\"$event.stopPropagation()\"\n [adaptivePosition]=\"false\"\n ></button>\n </label>\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Set Y-axis start to 0' | translate\"\n >\n <input\n name=\"setYaxisStartToZero\"\n type=\"checkbox\"\n formControlName=\"setYaxisStartToZero\"\n />\n <span></span>\n <span translate>Set Y-axis start to 0</span>\n <button\n class=\"btn-help\"\n [attr.aria-label]=\"\n 'Sets the Y-axis minimum to 0 for all data points with positive values. If any data point includes negative values, the axis may still extend below 0. Explicitly configured minimum and maximum values take precedence over this option.'\n | translate\n \"\n [popover]=\"\n 'Sets the Y-axis minimum to 0 for all data points with positive values. If any data point includes negative values, the axis may still extend below 0. Explicitly configured minimum and maximum values take precedence over this option.'\n | translate\n \"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n (click)=\"$event.stopPropagation()\"\n [adaptivePosition]=\"false\"\n ></button>\n </label>\n </c8y-form-group>\n </fieldset>\n <fieldset class=\"c8y-fieldset m-b-24 m-t-0\">\n <legend>{{ 'Alarms & events' | translate }}</legend>\n <c8y-form-group class=\"p-b-16 m-b-0 p-t-8 form-group-sm\">\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Show vertical line on every occurrence' | translate\"\n >\n <input\n name=\"displayMarkedLine\"\n type=\"checkbox\"\n formControlName=\"displayMarkedLine\"\n />\n <span></span>\n <span translate>Show vertical line on every occurrence</span>\n </label>\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Show icon when triggered' | translate\"\n >\n <input\n name=\"displayMarkedPoint\"\n type=\"checkbox\"\n formControlName=\"displayMarkedPoint\"\n />\n <span></span>\n <span translate>Show icon when triggered</span>\n @if (alarmsOrEventsHaveNoMatchingDps) {\n <button\n class=\"btn-clean m-l-8\"\n [attr.aria-label]=\"\n 'Some alarms or events have no matching data points. No icons will be shown for them.'\n | translate\n \"\n [tooltip]=\"\n 'Some alarms or events have no matching data points. No icons will be shown for them.'\n | translate\n \"\n container=\"body\"\n type=\"button\"\n (click)=\"$event.stopPropagation()\"\n [adaptivePosition]=\"false\"\n >\n <i\n class=\"text-warning\"\n c8yIcon=\"exclamation-triangle\"\n ></i>\n </button>\n }\n </label>\n </c8y-form-group>\n </fieldset>\n <fieldset class=\"c8y-fieldset m-b-24 m-t-0\">\n <legend>{{ 'Chart' | translate }}</legend>\n <c8y-form-group class=\"p-b-16 m-b-0 p-t-8 form-group-sm\">\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Show labels and units' | translate\"\n >\n <input\n name=\"showLabelAndUnit\"\n type=\"checkbox\"\n formControlName=\"showLabelAndUnit\"\n />\n <span></span>\n <span translate>Display labels and units on Y-axis</span>\n </label>\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Show slider' | translate\"\n >\n <input\n name=\"showSlider\"\n type=\"checkbox\"\n formControlName=\"showSlider\"\n />\n <span></span>\n <span translate>Show slider</span>\n </label>\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Show smooth lines' | translate\"\n >\n <input\n name=\"smoothLines\"\n type=\"checkbox\"\n formControlName=\"smoothLines\"\n />\n <span></span>\n <span translate>Show smooth lines</span>\n </label>\n </c8y-form-group>\n </fieldset>\n</form>\n\n<ng-template #dataPointsGraphPreview>\n @if (config?.displayMode && config.displayMode !== 'dashboard') {\n <c8y-local-controls\n [controls]=\"widgetControls\"\n [displayMode]=\"config.displayMode\"\n [config]=\"previewContextConfig()\"\n [disabled]=\"true\"\n ></c8y-local-controls>\n }\n\n @if (activeDatapointsExists) {\n <c8y-charts\n class=\"d-block p-relative\"\n [config]=\"config\"\n [alerts]=\"alerts\"\n [chartViewContext]=\"chartViewContext\"\n (timeRangeChangeOnRealtime)=\"updateTimeRangeOnRealtime($event)\"\n (configChangeOnZoomOut)=\"updateDashboardTimeContext($event)\"\n (updateAggregatedSliderDatapoint)=\"updateAggregatedSliderDatapoint($event)\"\n ></c8y-charts>\n }\n\n @if (!activeDatapointsExists) {\n <c8y-ui-empty-state\n class=\"d-block m-t-24\"\n [icon]=\"'search'\"\n [title]=\"'No data points selected' | translate\"\n [subtitle]=\"'Select data point to render content' | translate\"\n data-cy=\"datapoints-graph-list--empty-state-no-data-point-selected\"\n >\n <p c8y-guide-docs>\n <small\n translate\n ngNonBindable\n >\n Find out more in the\n <a c8y-guide-href=\"/docs/cockpit/widgets-collection/#data-point-graph\">\n user documentation</a\n >.\n </small>\n </p>\n </c8y-ui-empty-state>\n }\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: i1.EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "directive", type: i1.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: i1.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.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: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "component", type: i1.FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "component", type: i1.MessagesComponent, selector: "c8y-messages", inputs: ["show", "defaults", "helpMessage", "additionalMessages"] }, { kind: "directive", type: i1.RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { kind: "directive", type: i1.GuideHrefDirective, selector: "[c8y-guide-href]", inputs: ["c8y-guide-href"] }, { kind: "component", type: i1.GuideDocsComponent, selector: "[c8y-guide-docs]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i3.TooltipDirective, selector: "[tooltip], [tooltipHtml]", inputs: ["adaptivePosition", "tooltip", "placement", "triggers", "container", "containerClass", "boundariesElement", "isOpen", "isDisabled", "delay", "tooltipHtml", "tooltipPlacement", "tooltipIsOpen", "tooltipEnable", "tooltipAppendToBody", "tooltipAnimation", "tooltipClass", "tooltipContext", "tooltipPopupDelay", "tooltipFadeDuration", "tooltipTrigger"], outputs: ["tooltipChange", "onShown", "onHidden", "tooltipStateChanged"], exportAs: ["bs-tooltip"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "directive", type: i4.PopoverDirective, selector: "[popover]", inputs: ["adaptivePosition", "boundariesElement", "popover", "popoverContext", "popoverTitle", "placement", "outsideClick", "triggers", "container", "containerClass", "isOpen", "delay"], outputs: ["onShown", "onHidden"], exportAs: ["bs-popover"] }, { kind: "component", type: ChartsComponent, selector: "c8y-charts", inputs: ["config", "alerts", "chartViewContext"], outputs: ["configChangeOnZoomOut", "timeRangeChangeOnRealtime", "datapointOutOfSync", "datapointBackInSync", "updateAlarmsAndEvents", "isMarkedAreaEnabled", "finishLoading", "updateActiveDatapoints", "updateAggregatedSliderDatapoint"] }, { kind: "ngmodule", type: DatapointSelectorModule }, { kind: "ngmodule", type: AlarmEventSelectorModule }, { kind: "component", type: LocalControlsComponent, selector: "c8y-local-controls", inputs: ["controls", "displayMode", "config", "isLoading", "disabled", "emitRefresh"], outputs: ["configChange", "refresh"] }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DatapointsGraphWidgetConfigComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-datapoints-graph-widget-config', host: { class: 'd-contents' }, standalone: true, imports: [ CommonModule, CoreModule, FormsModule, TooltipModule, PopoverModule, ChartsComponent, DatapointSelectorModule, AlarmEventSelectorModule, LocalControlsComponent ], providers: [ ChartEventsService, ChartAlarmsService, ChartHelpersService, WidgetTimeContextDateRangeService ], template: "<form [formGroup]=\"formGroup\">\n <div class=\"form-group form-group-sm\">\n <label\n [title]=\"'Number of decimal places' | translate\"\n translate\n >\n Number of decimal places\n </label>\n <input\n class=\"form-control\"\n name=\"numberOfDecimalPlaces\"\n type=\"number\"\n formControlName=\"numberOfDecimalPlaces\"\n [placeholder]=\"'e.g. {{ example }}' | translate: { example: 1 }\"\n />\n <c8y-messages\n [show]=\"\n formGroup.controls?.numberOfDecimalPlaces?.touched &&\n formGroup?.controls?.numberOfDecimalPlaces?.errors\n \"\n ></c8y-messages>\n </div>\n <c8y-form-group class=\"form-group-sm\">\n <label>\n {{ 'Data point legend display' | translate }}\n </label>\n <div\n class=\"c8y-select-wrapper\"\n [formGroup]=\"formGroup\"\n >\n <select\n class=\"form-control\"\n [title]=\"'Data point legend display' | translate\"\n id=\"dataPointLegendDisplay\"\n formControlName=\"dataPointLegendDisplay\"\n >\n @for (option of legendDisplayOptions; track option) {\n <option [ngValue]=\"option.value\">\n {{ option.label | translate }}\n </option>\n }\n </select>\n </div>\n </c8y-form-group>\n</form>\n\n<form\n class=\"d-block p-t-8\"\n [formGroup]=\"formGroup\"\n>\n <label>{{ 'Display options' | translate }}</label>\n <fieldset class=\"c8y-fieldset m-b-24 m-t-0\">\n <legend>{{ 'Axis' | translate }}</legend>\n <c8y-form-group class=\"p-b-16 m-b-0 p-t-8 form-group-sm\">\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Y-axis helper lines' | translate\"\n >\n <input\n name=\"yAxisSplitLines\"\n type=\"checkbox\"\n formControlName=\"yAxisSplitLines\"\n />\n <span></span>\n <span translate>Y-axis helper lines</span>\n </label>\n <label\n class=\"c8y-checkbox\"\n [title]=\"'X-axis helper lines' | translate\"\n >\n <input\n name=\"xAxisSplitLines\"\n type=\"checkbox\"\n formControlName=\"xAxisSplitLines\"\n />\n <span></span>\n <span translate>X-axis helper lines</span>\n </label>\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Merge matching data points into single axis' | translate\"\n >\n <input\n name=\"mergeMatchingDatapoints\"\n type=\"checkbox\"\n formControlName=\"mergeMatchingDatapoints\"\n />\n <span></span>\n <span translate>Merge matching data points into single axis</span>\n <button\n class=\"btn-help\"\n [attr.aria-label]=\"\n 'Data points with the same min and max values will be merged into a single axis. The values must be defined in the data point configuration.'\n | translate\n \"\n [popover]=\"\n 'Data points with the same min and max values will be merged into a single axis. The values must be defined in the data point configuration.'\n | translate\n \"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n (click)=\"$event.stopPropagation()\"\n [adaptivePosition]=\"false\"\n ></button>\n </label>\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Force merge all data points into single axis' | translate\"\n >\n <input\n name=\"forceMergeDatapoints\"\n type=\"checkbox\"\n formControlName=\"forceMergeDatapoints\"\n />\n <span></span>\n <span translate>Force merge all data points into a single axis</span>\n <button\n class=\"btn-help\"\n [attr.aria-label]=\"\n 'All axes will be force merged to a single axis with the scale being set to the max and min value of all axes. It\\'s recommended to use this option for data points with similar values.'\n | translate\n \"\n [popover]=\"\n 'All axes will be force merged to a single axis with the scale being set to the max and min value of all axes. It\\'s recommended to use this option for data points with similar values.'\n | translate\n \"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n (click)=\"$event.stopPropagation()\"\n [adaptivePosition]=\"false\"\n ></button>\n </label>\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Set Y-axis start to 0' | translate\"\n >\n <input\n name=\"setYaxisStartToZero\"\n type=\"checkbox\"\n formControlName=\"setYaxisStartToZero\"\n />\n <span></span>\n <span translate>Set Y-axis start to 0</span>\n <button\n class=\"btn-help\"\n [attr.aria-label]=\"\n 'Sets the Y-axis minimum to 0 for all data points with positive values. If any data point includes negative values, the axis may still extend below 0. Explicitly configured minimum and maximum values take precedence over this option.'\n | translate\n \"\n [popover]=\"\n 'Sets the Y-axis minimum to 0 for all data points with positive values. If any data point includes negative values, the axis may still extend below 0. Explicitly configured minimum and maximum values take precedence over this option.'\n | translate\n \"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n (click)=\"$event.stopPropagation()\"\n [adaptivePosition]=\"false\"\n ></button>\n </label>\n </c8y-form-group>\n </fieldset>\n <fieldset class=\"c8y-fieldset m-b-24 m-t-0\">\n <legend>{{ 'Alarms & events' | translate }}</legend>\n <c8y-form-group class=\"p-b-16 m-b-0 p-t-8 form-group-sm\">\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Show vertical line on every occurrence' | translate\"\n >\n <input\n name=\"displayMarkedLine\"\n type=\"checkbox\"\n formControlName=\"displayMarkedLine\"\n />\n <span></span>\n <span translate>Show vertical line on every occurrence</span>\n </label>\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Show icon when triggered' | translate\"\n >\n <input\n name=\"displayMarkedPoint\"\n type=\"checkbox\"\n formControlName=\"displayMarkedPoint\"\n />\n <span></span>\n <span translate>Show icon when triggered</span>\n @if (alarmsOrEventsHaveNoMatchingDps) {\n <button\n class=\"btn-clean m-l-8\"\n [attr.aria-label]=\"\n 'Some alarms or events have no matching data points. No icons will be shown for them.'\n | translate\n \"\n [tooltip]=\"\n 'Some alarms or events have no matching data points. No icons will be shown for them.'\n | translate\n \"\n container=\"body\"\n type=\"button\"\n (click)=\"$event.stopPropagation()\"\n [adaptivePosition]=\"false\"\n >\n <i\n class=\"text-warning\"\n c8yIcon=\"exclamation-triangle\"\n ></i>\n </button>\n }\n </label>\n </c8y-form-group>\n </fieldset>\n <fieldset class=\"c8y-fieldset m-b-24 m-t-0\">\n <legend>{{ 'Chart' | translate }}</legend>\n <c8y-form-group class=\"p-b-16 m-b-0 p-t-8 form-group-sm\">\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Show labels and units' | translate\"\n >\n <input\n name=\"showLabelAndUnit\"\n type=\"checkbox\"\n formControlName=\"showLabelAndUnit\"\n />\n <span></span>\n <span translate>Display labels and units on Y-axis</span>\n </label>\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Show slider' | translate\"\n >\n <input\n name=\"showSlider\"\n type=\"checkbox\"\n formControlName=\"showSlider\"\n />\n <span></span>\n <span translate>Show slider</span>\n </label>\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Show smooth lines' | translate\"\n >\n <input\n name=\"smoothLines\"\n type=\"checkbox\"\n formControlName=\"smoothLines\"\n />\n <span></span>\n <span translate>Show smooth lines</span>\n </label>\n </c8y-form-group>\n </fieldset>\n</form>\n\n<ng-template #dataPointsGraphPreview>\n @if (config?.displayMode && config.displayMode !== 'dashboard') {\n <c8y-local-controls\n [controls]=\"widgetControls\"\n [displayMode]=\"config.displayMode\"\n [config]=\"previewContextConfig()\"\n [disabled]=\"true\"\n ></c8y-local-controls>\n }\n\n @if (activeDatapointsExists) {\n <c8y-charts\n class=\"d-block p-relative\"\n [config]=\"config\"\n [alerts]=\"alerts\"\n [chartViewContext]=\"chartViewContext\"\n (timeRangeChangeOnRealtime)=\"updateTimeRangeOnRealtime($event)\"\n (configChangeOnZoomOut)=\"updateDashboardTimeContext($event)\"\n (updateAggregatedSliderDatapoint)=\"updateAggregatedSliderDatapoint($event)\"\n ></c8y-charts>\n }\n\n @if (!activeDatapointsExists) {\n <c8y-ui-empty-state\n class=\"d-block m-t-24\"\n [icon]=\"'search'\"\n [title]=\"'No data points selected' | translate\"\n [subtitle]=\"'Select data point to render content' | translate\"\n data-cy=\"datapoints-graph-list--empty-state-no-data-point-selected\"\n >\n <p c8y-guide-docs>\n <small\n translate\n ngNonBindable\n >\n Find out more in the\n <a c8y-guide-href=\"/docs/cockpit/widgets-collection/#data-point-graph\">\n user documentation</a\n >.\n </small>\n </p>\n </c8y-ui-empty-state>\n }\n</ng-template>\n" }] }], ctorParameters: () => [], propDecorators: { config: [{ type: Input }], previewMapSet: [{ type: ViewChild, args: ['dataPointsGraphPreview'] }] } }); class DatapointsGraphWidgetViewComponent { set config(value) { // Special case: sync the config with migrated values. // This is needed to reflect migrated states in the widget config. const migratedConfig = this.widgetConfigMigrationService.migrateWidgetConfig(value); const newConfig = merge(value, migratedConfig); this.displayConfig = cloneDeep(newConfig); } get config() { throw Error('"config" property should not be referenced in view component to avoid mutating data.'); } constructor(translate, dashboardContextComponent, widgetConfigMigrationService, widgetTimeContextDateRangeService, cdr) { this.translate = translate; this.dashboardContextComponent = dashboardContextComponent; this.widgetConfigMigrationService = widgetConfigMigrationService; this.widgetTimeContextDateRangeService = widgetTimeContextDateRangeService; this.cdr = cdr; this.events = []; this.alarms = []; this.AGGREGATION_ICONS = AGGREGATION_ICONS; this.AGGREGATION_TEXTS = AGGREGATION_TEXTS; this.datapointsOutOfSync = new Map(); this.hasAtLeastOneDatapointActive = true; this.hasAtLeastOneAlarmActive = true; this.isMarkedAreaEnabled = false; this.loadedDatapoints = []; this.loadedAlarmsOrEvents = []; this.isLoading$ = new BehaviorSubject(false); this.isSliderBeingDragged$ = new BehaviorSubject(false); this.chartViewContext = CHART_VIEW_CONTEXT.WIDGET_VIEW; this.dashboardChild = inject(DashboardChildComponent); this.displayMode = signal(GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD, ...(ngDevMode ? [{ debugName: "displayMode" }] : [])); this.contextConfig = signal({}, ...(ngDevMode ? [{ debugName: "contextConfig" }] : [])); this.isLinkedToGlobal = signal(undefined, ...(ngDevMode ? [{ debugName: "isLinkedToGlobal" }] : [])); this.widgetControls = signal(PRESET_NAME.DATAPOINTS_GRAPH, ...(ngDevMode ? [{ debugName: "widgetControls" }] : [])); this.GLOBAL_CONTEXT_DISPLAY_MODE = GLOBAL_CONTEXT_DISPLAY_MODE; /** Combined number of datapoints, alarms, and events */ this.totalLegendItems = 0; /** Selectable items for the datapoints, alarms, events dropdown */ this.selectableItems = []; /** Selected items from the dropdown */ this.selectedItems = []; this.legendHelp = this.translate.instant(gettext(`<ul class="m-l-0 p-l-8 m-t-8 m-b-0"> <li> <b>Visibility:</b> use visibility icon to toggle data point, alarm, or event visibility on the chart. At least one data point is required to display a chart. </li> <li> <b>Alarm details</b> Click alarm legend item to highlight area between alarm raised timestamp and alarm cleared timestamp. You can also click alarm markline on chart to highlight alarm and to pause tooltip. Click on highlighted area or legend item to cancel highlighting. </li> </ul>`)); this.disableZoomInLabel = gettext('Disable zoom in'); this.enableZoomInLabel = gettext('Click to enable zoom, then click and drag on the desired area in the chart.'); this.hideDatapointLabel = gettext('Hide data point'); this.showDatapointLabel = gettext('Show data point'); this.destroy$ = new Subject(); this.widgetInstanceId = crypto.randomUUID(); } ngOnInit() { this.displayConfig?.datapoints?.forEach(dp => this.assignContextFromContextDashboard(dp)); this.displayConfig.realtime = this.computeRealtimeFlag(this.displayConfig); this.displayConfig.isRealtimeEnabled = this.displayConfig.realtime; this.totalLegendItems = (this.displayConfig?.datapoints?.length || 0) + (this.displayConfig?.alarmsEventsConfigs?.length || 0); this.loadedDatapoints = (this.displayConfig?.datapoints || []).filter(dp => dp.__active); this.loadedAlarmsOrEvents = (this.displayConfig?.alarmsEventsConfigs || []).filter(aOrE => aOrE.__active && !aOrE.__hidden); if (this.totalLegendItems > 5 || this.displayConfig?.dataPointLegendDisplay === 'dropdown') { this.selectableItems = this.buildSelectableItems(); // Initialize selectedItems with the currently active items this.selectedItems = this.selectableItems.filter(item => { if (item.value.type === 'DATAPOINT') { return !!item.value.original.__active; } else { return item.value.original.__active && !item.value.original.__hidden; } }); } const displayMode = this.displayConfig.displayMode || GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD; this.displayMode.set(displayMode); const initialContextConfig = { dateTimeContext: this.displayConfig.dateTimeContext, aggregation: this.displayConfig.aggregation, isAutoRefreshEnabled: this.displayConfig.isAutoRefreshEnabled, refreshInterval: this.displayConfig.refreshInterval, refreshOption: this.displayConfig.refreshOption }; this.contextConfig.set(initialContextConfig); this.updateExportConfig(); } onContextChange(event) { this.invalidateHiddenItemsCache(); this.contextConfig.set(event.context); const { context, diff } = event; const { dateTimeContext } = context; const { dateFrom, dateTo, interval } = dateTimeContext; const isRealtimeEnabled = this.computeRealtimeFlag(context); if (this.displayConfig.displayMode !== GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD) { const realtimeToggled = this.displayConfig?.realtime !== isRealtimeEnabled; if (realtimeToggled) { // When toggling realtime, we must: // 1. Spread ALL context properties (isAutoRefreshEnabled, refreshOption, etc.) // to keep the GC wrapper in sync. Otherwise stale isAutoRefreshEnabled on // displayConfig causes the wrapper's handleConfigChange() to silently // re-enable auto-refresh via setAutoRefreshEnabled() on the inline component. // 2. Use chart's actual visible range from dataZoom to avoid date jump. // GC's dateTimeContext dates don't match the chart's visible range — the chart // slides forward every 1s via ChartRealtimeService independently of GC. // 3. Return early to skip updateInitialTimeRange(null) which resets cached time range. const option = this.chartComponent?.echartsInstance?.getOption(); const dataZoom = option?.dataZoom?.[0]; const chartDateFrom = dataZoom?.startValue ? new Date(dataZoom.startValue).toISOString() : dateFrom; const chartDateTo = dataZoom?.endValue ? new Date(dataZoom.endValue).toISOString() : dateTo; this.displayConfig = { ...this.displayConfig, ...structuredClone(context), dateFrom: new Date(chartDateFrom), dateTo: new Date(chartDateTo), dateTimeContext: { ...context.dateTimeContext, dateFrom: chartDateFrom, dateTo: chartDateTo }, realtime: isRealtimeEnabled }; this.updateExportConfig(); return; } this.displayConfig = { ...this.displayConfig, ...structuredClone(context), dateFrom: new Date(dateFrom), dateTo: new Date(dateTo), interval, realtime: isRealtimeEnabled }; this.updateExportConfig(); if (this.isSliderBeingDragged$.value === true) { this.isSliderBeingDragged$.next(false); return; } this.widgetTimeContextDateRangeService.updateInitialTimeRange(null); return; } if (this.displayConfig?.realtime !== isRealtimeEnabled) { // Sync dates to avoid jumps in the chart this.displayConfig = merge(this.displayConfig, { dateTimeContext: structuredClone(context), dateFrom: new Date(dateFrom), dateTo: new Date(dateTo), interval }); this.updateExportConfig(); this.displayConfig = { ...this.displayConfig, realtime: isRealtimeEnabled }; } // Realtime is a special case, we need to block "automatic" emissions from the global context // GC in auto mode emits every 5 seconds. We only want to react to user changes. if (isRealtimeEnabled && diff?.dateTimeContext && Object.keys(diff).length === 1 && Object.keys(diff.dateTimeContext).length === 2) { // Sync dates to avoid jumps in the chart this.displayConfig = merge(this.displayConfig, { dateTimeContext: structuredClone(context), dateFrom: new Date(dateFrom), dateTo: new Date(dateTo), interval }); this.updateExportConfig(); return; } this.displayConfig = { ...this.displayConfig, ...structuredClone(context), dateFrom: new Date(dateFrom), dateTo: new Date(dateTo), interval }; this.updateExportConfig(); if (context.source === GLOBAL_CONTEXT_SOURCE.DASHBOARD && this.displayConfig.displayMode === GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD) { this.widgetTimeContextDateRangeService.updateInitialTimeRange(null); } } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } updateDashboardTimeContext(timeProps) { const chartOptions = this.chartComponent?.echartsInstance?.getOption(); const isEndZoomChanged = chartOptions?.dataZoom[0]?.end !== 100; // Check if widget is linked to global context const isLinked = this.isLinkedToGlobal() !== false; if (this.displayConfig.displayMode === GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD && isLinked) { const dateTimeContext = isEndZoomChanged ? { dateFrom: new Date(timeProps.dateFrom).toISOString(), dateTo: new Date(timeProps.dateTo).toISOString(), interval: timeProps.interval } : { dateFrom: new Date(timeProps.dateFrom).toISOString(), interval: timeProps.interval }; window.dispatchEvent(new CustomEvent(GLOBAL_CONTEXT_EVENTS.UPDATE_GLOBAL_CONTEXT_HISTORY, { detail: { type: GLOBAL_CONTEXT_EVENTS.UPDATE_GLOBAL_CONTEXT_HISTORY, payload: { dateTimeContext, isAutoRefreshEnabled: false, aggregation: this.displayConfig.aggregation, source: GLOBAL_CONTEXT_SOURCE.WIDGET, eventSourceId: this.widgetInstanceId }, timestamp: Date.now() } })); } else { this.isSliderBeingDragged$.next(true); const dateTimeContext = { dateFrom: new Date(timeProps.dateFrom), dateTo: new Date(timeProps.dateTo), interval: timeProps.interval }; this.isLinkedToGlobal.set(false); // Update displayConfig for internal state this.displayConfig = { ...this.displayConfig, dateTimeContext: structuredClone(dateTimeContext), dateFrom: new Date(timeProps.dateFrom), dateTo: new Date(timeProps.dateTo), interval: timeProps.interval, realtime: false, isAutoRefreshEnabled: false }; // Update context config signal this.contextConfig.set({ ...this.contextConfig(), dateTimeContext: structuredClone(dateTimeContext), isAutoRefreshEnabled: false }); } } getDa