UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1 lines • 71.4 kB
{"version":3,"file":"c8y-ngx-components-widgets-implementations-datapoints-graph.mjs","sources":["../../widgets/implementations/datapoints-graph/datapoints-graph-config/datapoints-graph-widget-config.component.ts","../../widgets/implementations/datapoints-graph/datapoints-graph-config/datapoints-graph-widget-config.component.html","../../widgets/implementations/datapoints-graph/datapoints-graph-view/datapoints-graph-widget-view.component.ts","../../widgets/implementations/datapoints-graph/datapoints-graph-view/datapoints-graph-widget-view.component.html","../../widgets/implementations/datapoints-graph/c8y-ngx-components-widgets-implementations-datapoints-graph.ts"],"sourcesContent":["import {\n Component,\n inject,\n Input,\n OnDestroy,\n OnInit,\n Optional,\n TemplateRef,\n ViewChild\n} from '@angular/core';\nimport { FormBuilder, NgForm, Validators } from '@angular/forms';\nimport { Observable } from 'rxjs/internal/Observable';\nimport {\n CommonModule,\n CoreModule,\n DynamicComponentAlertAggregator,\n FormsModule,\n gettext,\n OnBeforeSave,\n WidgetTimeContextDateRangeService\n} from '@c8y/ngx-components';\nimport { TranslateService } from '@ngx-translate/core';\nimport { takeUntil } from 'rxjs/operators';\nimport { Subject } from 'rxjs';\nimport {\n DatapointAttributesFormConfig,\n DatapointSelectorModalOptions,\n DatapointSelectorModule,\n KPIDetails\n} from '@c8y/ngx-components/datapoint-selector';\nimport { omit } from 'lodash-es';\nimport { aggregationType } from '@c8y/client';\nimport {\n ContextDashboardComponent,\n WidgetConfigComponent,\n WidgetConfigService\n} from '@c8y/ngx-components/context-dashboard';\nimport {\n AlarmDetails,\n AlarmEventSelectorModule,\n EventDetails\n} from '@c8y/ngx-components/alarm-event-selector';\nimport { TooltipModule } from 'ngx-bootstrap/tooltip';\nimport { PopoverModule } from 'ngx-bootstrap/popover';\nimport {\n ChartAlarmsService,\n ChartEventsService,\n ChartsComponent,\n DatapointsGraphKPIDetails,\n DatapointsGraphWidgetConfig,\n DatapointsGraphWidgetTimeProps,\n DATE_SELECTION_EXTENDED,\n TimeContextProps\n} from '@c8y/ngx-components/echart';\nimport { TimeContextComponent } from '@c8y/ngx-components/time-context';\nimport { Interval } from '@c8y/ngx-components/interval-picker';\n\n@Component({\n selector: 'c8y-datapoints-graph-widget-config',\n host: { class: 'd-contents' },\n templateUrl: './datapoints-graph-widget-config.component.html',\n standalone: true,\n imports: [\n CommonModule,\n CoreModule,\n FormsModule,\n TooltipModule,\n PopoverModule,\n ChartsComponent,\n DatapointSelectorModule,\n AlarmEventSelectorModule,\n TimeContextComponent\n ],\n providers: [ChartEventsService, ChartAlarmsService]\n})\nexport class DatapointsGraphWidgetConfigComponent implements OnInit, OnBeforeSave, OnDestroy {\n @Input() config: DatapointsGraphWidgetConfig | undefined;\n @ViewChild('dataPointsGraphPreview')\n set previewMapSet(template: TemplateRef<any>) {\n if (template) {\n this.widgetConfigService.setPreview(template);\n return;\n }\n this.widgetConfigService.setPreview(null);\n }\n\n private readonly formBuilder = inject(FormBuilder);\n private readonly form = inject(NgForm);\n private readonly translate = inject(TranslateService);\n private readonly widgetTimeContextDateRangeService = inject(WidgetTimeContextDateRangeService);\n private readonly widgetConfigService = inject(WidgetConfigService);\n\n alerts: DynamicComponentAlertAggregator | undefined;\n formGroup: ReturnType<DatapointsGraphWidgetConfigComponent['initForm']>;\n DATE_SELECTION = DATE_SELECTION_EXTENDED;\n dateSelection: DATE_SELECTION_EXTENDED | undefined;\n dateSelectionHelp = this.translate.instant(\n gettext(`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>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 <li>\n <b>Dashboard time range:</b>\n restricts date selection to the global dashboard configuration only\n </li>\n </ul>`)\n );\n datapointSelectDefaultFormOptions: Partial<DatapointAttributesFormConfig> = {\n showRange: true,\n showChart: true\n };\n datapointSelectionConfig: Partial<DatapointSelectorModalOptions> = {};\n activeDatapointsExists = false;\n alarmsOrEventsHaveNoMatchingDps = false;\n timeProps: DatapointsGraphWidgetTimeProps | undefined;\n private destroy$ = new Subject<void>();\n\n constructor(\n @Optional() private widgetConfig: WidgetConfigComponent,\n @Optional() private dashboardContextComponent: ContextDashboardComponent\n ) {\n this.formGroup = this.initForm();\n }\n\n ngOnInit() {\n this.config?.datapoints?.forEach(dp => this.assignContextFromContextDashboard(dp));\n this.form.form.addControl('config', this.formGroup);\n this.formGroup.patchValue(this.config || {});\n this.formGroup.controls.alarms.setValue(\n this.config?.alarmsEventsConfigs?.filter(ae => ae.timelineType === 'ALARM') as AlarmDetails[]\n );\n this.formGroup.controls.events.setValue(\n this.config?.alarmsEventsConfigs?.filter(ae => ae.timelineType === 'EVENT') as EventDetails[]\n );\n\n this.initDateSelection();\n this.setActiveDatapointsExists();\n this.checkForMatchingDatapoints();\n this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => {\n this.config = {\n ...value,\n alarmsEventsConfigs: [\n ...(this.formGroup.value.alarms || []),\n ...(this.formGroup.value.events || [])\n ]\n };\n this.setActiveDatapointsExists();\n this.checkForMatchingDatapoints();\n });\n\n if (this.config?.widgetInstanceGlobalTimeContext) {\n this.updateDashboardTimeContext(this.widgetTimeContextDateRangeService.initialTimeRange());\n }\n\n if (this.config.dateFrom && this.config.dateTo) {\n this.timeProps = {\n dateFrom: new Date(this.config?.dateFrom),\n dateTo: new Date(this.config?.dateTo),\n interval: this.config?.interval,\n realtime: this.config?.realtime,\n aggregation: this.config?.realtime ? null : this.config?.aggregation\n };\n }\n }\n\n ngOnDestroy() {\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n onBeforeSave(\n config?: DatapointsGraphWidgetConfig\n ): boolean | Promise<boolean> | Observable<boolean> {\n if (this.formGroup.valid && config) {\n Object.assign(config, omit(this.formGroup.value, ['alarms', 'events']), {\n alarmsEventsConfigs: [\n ...(this.formGroup.value.alarms || []),\n ...(this.formGroup.value.events || [])\n ]\n });\n\n return true;\n }\n return false;\n }\n\n timePropsChanged(timeProps: TimeContextProps): void {\n if (timeProps.realtime !== this.config.realtime) {\n this.formGroup.patchValue({ realtime: timeProps.realtime });\n }\n if (timeProps.realtime) {\n if (timeProps.currentDateContextInterval !== this.formGroup.value.interval) {\n this.formGroup.patchValue({ interval: timeProps.currentDateContextInterval });\n }\n return;\n }\n const patchValues = {\n dateFrom: new Date(timeProps.currentDateContextFromDate),\n dateTo: new Date(timeProps.currentDateContextToDate),\n interval: timeProps.currentDateContextInterval,\n ...(timeProps.aggregation && { aggregation: timeProps.aggregation }),\n ...(timeProps.realtime && { realtime: timeProps.realtime })\n };\n\n this.formGroup.patchValue(patchValues);\n }\n\n updateDashboardTimeContext(timeProps: DatapointsGraphWidgetTimeProps): void {\n const initialTimeRange = {\n dateFrom: timeProps.dateFrom,\n dateTo: timeProps.dateTo,\n interval: timeProps.interval || 'custom'\n };\n if (!this.widgetTimeContextDateRangeService.initialTimeRange()) {\n this.widgetTimeContextDateRangeService.updateInitialTimeRange(initialTimeRange);\n }\n this.formGroup.patchValue({ ...timeProps, ...initialTimeRange });\n }\n\n updateTimeRangeOnRealtime(\n timeRange: Pick<DatapointsGraphWidgetConfig, 'dateFrom' | 'dateTo'>\n ): void {\n this.formGroup.patchValue(timeRange, { emitEvent: false });\n }\n\n dateSelectionChange(dateSelection: DATE_SELECTION_EXTENDED): void {\n this.dateSelection = dateSelection;\n\n if (dateSelection === DATE_SELECTION_EXTENDED.CONFIG) {\n this.formGroup.controls.displayDateSelection.enable();\n this.formGroup.patchValue({ widgetInstanceGlobalTimeContext: false });\n return;\n }\n\n // displayDateSelection should be false and disabled when dateSelection is not CONFIG\n this.formGroup.controls.displayDateSelection.disable();\n this.formGroup.patchValue({\n widgetInstanceGlobalTimeContext: true,\n realtime: false,\n displayDateSelection: false\n });\n }\n\n private assignContextFromContextDashboard(datapoint: KPIDetails) {\n if (!this.dashboardContextComponent?.isDeviceTypeDashboard) {\n return;\n }\n const context = this.widgetConfig?.context;\n if (context?.id) {\n const { name, id } = context;\n datapoint.__target = { name, id };\n this.datapointSelectionConfig.contextAsset = { id };\n }\n }\n\n private checkForMatchingDatapoints(): void {\n const allMatch = this.config?.alarmsEventsConfigs?.every(ae =>\n this.formGroup.value.datapoints?.some(dp => dp.__target?.id === ae.__target?.id)\n );\n\n queueMicrotask(() => {\n if (allMatch) {\n this.alarmsOrEventsHaveNoMatchingDps = false;\n } else {\n this.alarmsOrEventsHaveNoMatchingDps = true;\n }\n });\n }\n\n private initForm() {\n const form = this.formBuilder.group({\n datapoints: [\n [] as DatapointsGraphKPIDetails[],\n [Validators.required, Validators.minLength(1)]\n ],\n alarms: [[] as AlarmDetails[]],\n events: [[] as EventDetails[]],\n displayMarkedLine: [true, []],\n displayMarkedPoint: [true, []],\n mergeMatchingDatapoints: [true, []],\n forceMergeDatapoints: [false, []],\n showLabelAndUnit: [true, []],\n displayDateSelection: [false, []],\n displayAggregationSelection: [false, []],\n widgetInstanceGlobalTimeContext: [false, []],\n canDecoupleGlobalTimeContext: [false, []],\n dateFrom: [null as unknown as Date, []],\n dateTo: [null as unknown as Date, []],\n interval: ['days' as Interval['id'], [Validators.required]],\n aggregation: [null as aggregationType | null, []],\n realtime: [false, [Validators.required]],\n showSlider: [true, [Validators.required]],\n yAxisSplitLines: [false, [Validators.required]],\n xAxisSplitLines: [false, [Validators.required]],\n numberOfDecimalPlaces: [2, [Validators.required, Validators.min(0), Validators.max(10)]]\n });\n return form;\n }\n\n private initDateSelection(): void {\n if (!this.config?.widgetInstanceGlobalTimeContext) {\n this.dateSelection = DATE_SELECTION_EXTENDED.CONFIG;\n return;\n }\n\n this.dateSelection = DATE_SELECTION_EXTENDED.DASHBOARD_CONTEXT;\n this.formGroup.controls.displayDateSelection.disable();\n }\n\n private setActiveDatapointsExists() {\n this.activeDatapointsExists =\n (this.config?.datapoints?.filter(dp => dp.__active)?.length || 0) > 0;\n }\n}\n","<div class=\"no-card-context\">\n <div class=\"conf-col\">\n <form [formGroup]=\"formGroup\">\n <c8y-datapoint-selection-list\n class=\"separator-bottom d-block\"\n name=\"datapoints\"\n [minActiveCount]=\"1\"\n [defaultFormOptions]=\"datapointSelectDefaultFormOptions\"\n [config]=\"datapointSelectionConfig\"\n formControlName=\"datapoints\"\n ></c8y-datapoint-selection-list>\n\n <c8y-alarm-event-selection-list\n class=\"separator-bottom d-block\"\n name=\"alarms\"\n formControlName=\"alarms\"\n [timelineType]=\"'ALARM'\"\n [datapoints]=\"config?.datapoints\"\n ></c8y-alarm-event-selection-list>\n\n <c8y-alarm-event-selection-list\n class=\"bg-inherit\"\n name=\"events\"\n formControlName=\"events\"\n [timelineType]=\"'EVENT'\"\n [datapoints]=\"config?.datapoints\"\n ></c8y-alarm-event-selection-list>\n\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 </form>\n </div>\n\n <div class=\"p-t-32 separator-top\">\n <div class=\"d-col fit-h\">\n <div class=\"form-group p-t-8 form-group-sm d-flex a-i-center m-b-8\">\n <div class=\"d-flex a-i-center m-r-4\">\n <label\n class=\"m-b-0\"\n translate\n >\n Date selection\n </label>\n <button\n class=\"btn-help btn-help--sm\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"dateSelectionHelpTemplate\"\n placement=\"bottom\"\n triggers=\"focus\"\n container=\"body\"\n [adaptivePosition]=\"false\"\n ></button>\n </div>\n <div class=\"c8y-select-wrapper\">\n <select\n class=\"form-control input-sm\"\n [ngModel]=\"dateSelection\"\n (ngModelChange)=\"dateSelectionChange($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n >\n <option\n title=\"{{ 'Dashboard time range' | translate }}\"\n [value]=\"DATE_SELECTION.DASHBOARD_CONTEXT\"\n >\n {{ 'Dashboard time range' | translate }}\n </option>\n <option\n title=\"{{ 'Widget configuration' | translate }}\"\n [value]=\"DATE_SELECTION.CONFIG\"\n >\n {{ 'Widget configuration' | translate }}\n </option>\n </select>\n <span></span>\n </div>\n </div>\n <label\n class=\"text-12\"\n *ngIf=\"dateSelection === DATE_SELECTION.CONFIG\"\n >\n {{ 'Options' | translate }}\n </label>\n <c8y-time-context\n *ngIf=\"dateSelection === DATE_SELECTION.CONFIG\"\n [changedDateContext]=\"timeProps\"\n [controlsAvailable]=\"{\n realtime: true,\n timeRange: true,\n interval: true,\n aggregation: true\n }\"\n (contextChange)=\"timePropsChanged($event)\"\n ></c8y-time-context>\n\n <form\n class=\"d-block p-t-16\"\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-dot m-l-8\"\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 [tooltip]=\"\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 container=\"body\"\n type=\"button\"\n (click)=\"$event.stopPropagation()\"\n [adaptivePosition]=\"false\"\n >\n <i\n class=\"text-info\"\n c8yIcon=\"info\"\n ></i>\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 datapoints into a single axis</span>\n <button\n class=\"btn-dot m-l-8\"\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 [tooltip]=\"\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 container=\"body\"\n type=\"button\"\n (click)=\"$event.stopPropagation()\"\n [adaptivePosition]=\"false\"\n >\n <i\n class=\"text-info\"\n c8yIcon=\"info\"\n ></i>\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 <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 *ngIf=\"alarmsOrEventsHaveNoMatchingDps\"\n (click)=\"$event.stopPropagation()\"\n [adaptivePosition]=\"false\"\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 </button>\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 </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 <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 *ngIf=\"alarmsOrEventsHaveNoMatchingDps\"\n (click)=\"$event.stopPropagation()\"\n [adaptivePosition]=\"false\"\n >\n <i\n class=\"text-warning\"\n c8yIcon=\"exclamation-triangle\"\n ></i>\n </button>\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]=\"'Enable date selection in the widget view.' | translate\"\n >\n <input\n name=\"displayDateSelection\"\n type=\"checkbox\"\n formControlName=\"displayDateSelection\"\n />\n <span></span>\n <span translate>Date selection in the widget view.</span>\n <button\n class=\"btn-clean m-l-8\"\n [attr.aria-label]=\"\n 'Date selection in widget view is not possible when using dashboard time range.'\n | translate\n \"\n [tooltip]=\"\n 'Date selection in widget view is not possible when using dashboard time range.'\n | translate\n \"\n container=\"body\"\n type=\"button\"\n *ngIf=\"dateSelection === DATE_SELECTION.DASHBOARD_CONTEXT\"\n (click)=\"$event.stopPropagation()\"\n [adaptivePosition]=\"false\"\n >\n <i\n class=\"text-info\"\n c8yIcon=\"info\"\n ></i>\n </button>\n </label>\n <label\n class=\"c8y-checkbox\"\n [title]=\"'Aggregation selection' | translate\"\n >\n <input\n name=\"displayAggregationSelection\"\n type=\"checkbox\"\n formControlName=\"displayAggregationSelection\"\n />\n <span></span>\n <span translate>Aggregation selection</span>\n </label>\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 </c8y-form-group>\n </fieldset>\n </form>\n </div>\n </div>\n</div>\n\n<ng-template #dataPointsGraphPreview>\n <c8y-charts\n class=\"d-block p-relative\"\n *ngIf=\"activeDatapointsExists\"\n [config]=\"config\"\n [alerts]=\"alerts\"\n (timeRangeChangeOnRealtime)=\"updateTimeRangeOnRealtime($event)\"\n (configChangeOnZoomOut)=\"updateDashboardTimeContext($event)\"\n ></c8y-charts>\n\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 chart' | translate\"\n *ngIf=\"!activeDatapointsExists\"\n ></c8y-ui-empty-state>\n</ng-template>\n\n<ng-template #dateSelectionHelpTemplate>\n <div [innerHTML]=\"dateSelectionHelp\"></div>\n</ng-template>\n","import {\n Component,\n Input,\n OnChanges,\n OnDestroy,\n Optional,\n SimpleChanges,\n ViewChild\n} from '@angular/core';\nimport {\n AGGREGATION_ICONS,\n AGGREGATION_TEXTS,\n CoreModule,\n DynamicComponentAlertAggregator,\n gettext,\n WidgetTimeContextDateRangeService\n} from '@c8y/ngx-components';\nimport { cloneDeep } from 'lodash-es';\nimport { FormBuilder, Validators } from '@angular/forms';\nimport { takeUntil } from 'rxjs/operators';\nimport {\n ALARM_STATUS_LABELS,\n AlarmStatusType,\n SeveritySettings,\n aggregationType\n} from '@c8y/client';\nimport type { KPIDetails } from '@c8y/ngx-components/datapoint-selector';\nimport { TranslateService } from '@ngx-translate/core';\nimport { ContextDashboardComponent } from '@c8y/ngx-components/context-dashboard';\nimport { A11yModule } from '@angular/cdk/a11y';\nimport { CommonModule } from '@angular/common';\nimport {\n ChartsComponent,\n DatapointsGraphKPIDetails,\n DatapointsGraphWidgetConfig,\n DatapointsGraphWidgetTimeProps,\n SeverityType,\n AlarmDetailsExtended,\n AlarmOrEventExtended,\n EventDetailsExtended,\n ChartEventsService,\n ChartAlarmsService,\n TimeContextProps\n} from '@c8y/ngx-components/echart';\nimport { TooltipModule } from 'ngx-bootstrap/tooltip';\nimport { BsDropdownModule } from 'ngx-bootstrap/dropdown';\nimport { PopoverModule } from 'ngx-bootstrap/popover';\nimport { AlarmsModule } from '@c8y/ngx-components/alarms';\nimport { Subject } from 'rxjs';\nimport { TimeContextComponent } from '@c8y/ngx-components/time-context';\nimport { Interval } from '@c8y/ngx-components/interval-picker';\n\n@Component({\n selector: 'c8y-datapoints-graph-widget-view',\n templateUrl: './datapoints-graph-widget-view.component.html',\n standalone: true,\n imports: [\n A11yModule,\n CommonModule,\n ChartsComponent,\n CoreModule,\n TooltipModule,\n BsDropdownModule,\n PopoverModule,\n AlarmsModule,\n TimeContextComponent\n ],\n providers: [ChartEventsService, ChartAlarmsService]\n})\nexport class DatapointsGraphWidgetViewComponent implements OnChanges, OnDestroy {\n events: EventDetailsExtended[] = [];\n alarms: AlarmDetailsExtended[] = [];\n AGGREGATION_ICONS = AGGREGATION_ICONS;\n AGGREGATION_TEXTS = AGGREGATION_TEXTS;\n alerts: DynamicComponentAlertAggregator | undefined;\n datapointsOutOfSync = new Map<DatapointsGraphKPIDetails, boolean>();\n timeProps: DatapointsGraphWidgetTimeProps | undefined;\n hasAtLeastOneDatapointActive = true;\n hasAtLeastOneAlarmActive = true;\n timeControlsFormGroup: ReturnType<DatapointsGraphWidgetViewComponent['initForm']>;\n isMarkedAreaEnabled = false;\n loadedDatapoints: DatapointsGraphKPIDetails[] = [];\n loadedAlarmsOrEvents: AlarmOrEventExtended[] = [];\n /*\n * @description: The type of alarm that has marked area enabled.\n */\n enabledMarkedAreaAlarmType: string | undefined;\n\n @Input() set config(value: DatapointsGraphWidgetConfig) {\n this.displayConfig = cloneDeep(value);\n }\n get config(): never {\n throw Error(\n '\"config\" property should not be referenced in view component to avoid mutating data.'\n );\n }\n @ViewChild(ChartsComponent) chartComponent!: ChartsComponent;\n displayConfig: DatapointsGraphWidgetConfig | undefined;\n legendHelp = this.translate.instant(\n gettext(`<ul class=\"m-l-0 p-l-8 m-t-8 m-b-0\">\n <li>\n <b>Visibility:</b>\n use visibility icon to toggle datapoint, alarm or event visibility on chart. At least one datapoint is required to display chart.\n </li>\n <li>\n <b>Alarm details</b>\n Click alarm legend item to highlight area between alarm raised timestamp and alarm cleared timestamp.\n 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.\n </li>\n </ul>`)\n );\n readonly disableZoomInLabel = gettext('Disable zoom in');\n readonly enableZoomInLabel = gettext(\n 'Click to enable zoom, then click and drag on the desired area in the chart.'\n );\n readonly hideDatapointLabel = gettext('Hide data point');\n readonly showDatapointLabel = gettext('Show data point');\n private destroy$ = new Subject<void>();\n\n constructor(\n private formBuilder: FormBuilder,\n private translate: TranslateService,\n private widgetTimeContextDateRangeService: WidgetTimeContextDateRangeService,\n @Optional() private dashboardContextComponent: ContextDashboardComponent\n ) {\n this.timeControlsFormGroup = this.initForm();\n this.timeControlsFormGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => {\n this.displayConfig = { ...this.displayConfig, ...value };\n });\n }\n\n ngOnInit() {\n this.displayConfig?.datapoints?.forEach(dp => this.assignContextFromContextDashboard(dp));\n this.displayConfig?.alarmsEventsConfigs?.forEach(alarmOrEvent =>\n this.assignContextFromContextDashboard(alarmOrEvent)\n );\n if (this.displayConfig.dateFrom && this.displayConfig.dateTo) {\n this.timeProps = {\n dateFrom: new Date(this.displayConfig?.dateFrom),\n dateTo: new Date(this.displayConfig?.dateTo),\n interval: this.displayConfig?.interval,\n realtime: this.displayConfig?.realtime,\n aggregation: this.displayConfig?.realtime ? null : this.displayConfig?.aggregation\n };\n }\n this.loadedDatapoints = this.displayConfig?.datapoints?.filter(dp => dp.__active) || [];\n this.loadedAlarmsOrEvents =\n this.displayConfig?.alarmsEventsConfigs?.filter(alarmOrEvent => alarmOrEvent.__active) || [];\n }\n\n ngOnDestroy() {\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n ngOnChanges(changes: SimpleChanges) {\n this.timeControlsFormGroup.patchValue(this.displayConfig || {});\n const config: DatapointsGraphWidgetConfig = changes['config']?.currentValue;\n if (config?.date && config?.widgetInstanceGlobalTimeContext && this.displayConfig?.date) {\n if (!this.displayConfig.sliderChange) {\n this.widgetTimeContextDateRangeService.updateInitialTimeRange(null);\n }\n const patchValues = {\n dateFrom: new Date(this.displayConfig?.date[0].toISOString()),\n dateTo: new Date(this.displayConfig?.date[1].toISOString()),\n interval: this.displayConfig?.interval,\n ...(this.displayConfig?.aggregation && { aggregation: this.displayConfig?.aggregation }),\n ...(this.displayConfig?.realtime && { realtime: this.displayConfig?.realtime })\n };\n this.timeControlsFormGroup.patchValue(patchValues);\n }\n }\n\n timePropsChanged(timeProps: TimeContextProps): void {\n if (timeProps.realtime && !this.displayConfig.sliderChange) {\n this.widgetTimeContextDateRangeService.updateInitialTimeRange(null);\n }\n\n if (\n timeProps.realtime !== this.displayConfig?.realtime ||\n (timeProps.realtime === this.displayConfig?.realtime) === false\n ) {\n const patchValues = {\n dateFrom: new Date(timeProps.currentDateContextFromDate),\n dateTo: new Date(timeProps.currentDateContextToDate),\n interval: timeProps.currentDateContextInterval,\n aggregation: timeProps.aggregation,\n realtime: timeProps.realtime\n };\n this.timeControlsFormGroup.patchValue(patchValues);\n }\n }\n\n updateDashboardTimeContext(timeProps: DatapointsGraphWidgetTimeProps): void {\n if (this.displayConfig?.widgetInstanceGlobalTimeContext) {\n this.widgetTimeContextDateRangeService.emitPropertyUpdate(timeProps);\n }\n this.timeControlsFormGroup.patchValue(timeProps);\n this.timeProps = { ...timeProps, realtime: false };\n }\n\n updateTimeRangeOnRealtime(\n timeRange: Pick<DatapointsGraphWidgetConfig, 'dateFrom' | 'dateTo'>\n ): void {\n this.timeControlsFormGroup.patchValue(timeRange, { emitEvent: false });\n }\n\n toggleChart(datapoint: DatapointsGraphKPIDetails): void {\n if (\n this.displayConfig?.datapoints?.filter(dp => dp.__active).length === 1 &&\n datapoint.__active\n ) {\n // at least 1 datapoint should be active\n this.hasAtLeastOneDatapointActive = false;\n return;\n }\n datapoint.__active = !datapoint.__active;\n this.hasAtLeastOneDatapointActive = true;\n if (!this.loadedDatapoints.find(dp => dp.label === datapoint.label)) {\n this.loadedDatapoints.push(datapoint);\n this.displayConfig = { ...this.displayConfig };\n return;\n }\n this.chartComponent.toggleDatapointSeriesVisibility(datapoint);\n }\n\n handleDatapointOutOfSync(dpOutOfSync: DatapointsGraphKPIDetails): void {\n const key = (dp: KPIDetails) => dp.__target?.id + dp.fragment + dp.series;\n const dpMatch = this.displayConfig?.datapoints?.find(dp => key(dp) === key(dpOutOfSync));\n if (!dpMatch) {\n return;\n }\n this.datapointsOutOfSync.set(dpMatch, true);\n }\n\n toggleMarkedArea(alarm: AlarmDetailsExtended): void {\n this.enabledMarkedAreaAlarmType = alarm.filters.type;\n const params = {\n data: {\n itemType: alarm.filters.type\n }\n };\n this.chartComponent.onChartClick(params);\n }\n\n toggleAlarmEventType(alarmOrEvent: AlarmOrEventExtended): void {\n if (alarmOrEvent.timelineType === 'ALARM') {\n this.alarms = this.alarms.map(alarm => {\n if (alarm.filters.type === alarmOrEvent.filters.type) {\n alarm.__hidden = !alarm.__hidden;\n }\n return alarm;\n });\n } else {\n this.events = this.events.map(event => {\n if (event.filters.type === alarmOrEvent.filters.type) {\n event.__hidden = !event.__hidden;\n }\n return event;\n });\n }\n if (!this.loadedAlarmsOrEvents.find(aOrE => aOrE.filters.type === alarmOrEvent.filters.type)) {\n this.loadedAlarmsOrEvents.push(alarmOrEvent);\n this.displayConfig = { ...this.displayConfig };\n return;\n }\n this.chartComponent.toggleAlarmEventSeriesVisibility(alarmOrEvent);\n }\n\n updateAlarmsAndEvents(alarmsEventsConfigs: AlarmOrEventExtended[]): void {\n this.alarms = alarmsEventsConfigs.filter(\n alarm => alarm.timelineType === 'ALARM'\n ) as AlarmDetailsExtended[];\n this.events = alarmsEventsConfigs.filter(\n event => event.timelineType === 'EVENT'\n ) as EventDetailsExtended[];\n if (this.alarms.length === 0 || !this.alarms.find(alarm => alarm.__active)) {\n this.hasAtLeastOneAlarmActive = false;\n }\n }\n\n filterSeverity(eventTarget: any): void {\n this.alarms = this.alarms.map(alarm => {\n if (!alarm.__severity) {\n alarm.__severity = [];\n }\n alarm.__severity = Object.keys(eventTarget.severityOptions).filter(\n (severity): severity is keyof SeveritySettings =>\n eventTarget.severityOptions[severity as keyof SeveritySettings]\n ) as SeverityType[];\n\n if (!alarm.__status) {\n alarm.__status = [];\n }\n const statuses = Object.keys(ALARM_STATUS_LABELS) as AlarmStatusType[];\n const filteredStatuses = eventTarget.showCleared\n ? statuses\n : statuses.filter(status => status !== 'CLEARED');\n alarm.__status = filteredStatuses;\n return alarm;\n });\n this.displayConfig = { ...this.displayConfig };\n }\n\n private assignContextFromContextDashboard(\n dpOrAlarmOrEvent: KPIDetails | AlarmOrEventExtended\n ): void {\n if (!this.dashboardContextComponent?.isDeviceTypeDashboard) {\n return;\n }\n const context = this.dashboardContextComponent?.context;\n if (context?.id) {\n const { name, id } = context;\n dpOrAlarmOrEvent.__target = { name, id };\n }\n }\n\n private initForm() {\n const form = this.formBuilder.group({\n dateFrom: [undefined as unknown as Date, [Validators.required]],\n dateTo: [undefined as unknown as Date, [Validators.required]],\n interval: [\n this.displayConfig?.interval || ('hours' as Interval['id']),\n [Validators.required]\n ],\n aggregation: [null as aggregationType | null, []],\n realtime: [false, [Validators.required]],\n widgetInstanceGlobalTimeContext: [false, []]\n });\n form.patchValue(this.displayConfig || {});\n return form;\n }\n}\n","<div class=\"p-l-16 p-r-16\">\n <div class=\"d-flex gap-16 a-i-start\">\n <div\n class=\"btn-group btn-group-sm flex-no-shrink\"\n *ngIf=\"\n !displayConfig?.widgetInstanceGlobalTimeContext && displayConfig?.datapoints.length > 0 && displayConfig?.displayDateSelection || displayConfig?.displayAggregationSelection\n \"\n >\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Aggregation' | translate\"\n tooltip=\"{{\n (displayConfig?.aggregation\n ? AGGREGATION_TEXTS[displayConfig.aggregation]\n : AGGREGATION_TEXTS.undefined\n ) | translate\n }}\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n [adaptivePosition]=\"false\"\n [delay]=\"500\"\n >\n <i\n class=\"icon-14\"\n [c8yIcon]=\"\n displayConfig?.aggregation\n ? AGGREGATION_ICONS[displayConfig.aggregation]\n : AGGREGATION_ICONS.undefined\n \"\n ></i>\n </button>\n\n <c8y-time-context\n class=\"d-contents\"\n (contextChange)=\"timePropsChanged($event)\"\n [changedDateContext]=\"timeProps\"\n [controlsAvailable]=\"{\n realtime: true,\n timeRange: displayConfig?.displayDateSelection,\n interval: displayConfig?.displayDateSelection,\n aggregation: displayConfig?.displayAggregationSelection\n }\"\n ></c8y-time-context>\n </div>\n <c8y-alarms-filter\n class=\"d-contents form-group-sm\"\n *ngIf=\"hasAtLeastOneAlarmActive\"\n (filterApplied)=\"filterSeverity($event)\"\n ></c8y-alarms-filter>\n\n <div class=\"m-l-auto btn-group btn-group-sm flex-no-shrink\">\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Save as image' | translate\"\n tooltip=\"{{ 'Save as image' | translate }}\"\n container=\"body\"\n type=\"button\"\n (click)=\"chart.saveAsImage()\"\n [adaptivePosition]=\"false\"\n >\n <i\n class=\"icon-14\"\n c8yIcon=\"image-file-checked\"\n ></i>\n </button>\n </div>\n </div>\n <div\n class=\"d-flex\"\n style=\"align-items: center\"\n *ngIf=\"displayConfig?.datapoints.length > 0\"\n >\n <button\n class=\"btn-help btn-help--sm m-r-8\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"legendHelpTemplate\"\n placement=\"bottom\"\n triggers=\"focus\"\n container=\"body\"\n [adaptivePosition]=\"false\"\n ></button>\n <div class=\"inner-scroll\">\n <div class=\"flex-grow p-t-8 d-flex a-i-start gap-8 p-b-4\">\n <div\n class=\"c8y-datapoint-pill flex-no-shrink\"\n title=\"{{ datapoint.label }} - {{ datapoint.__target.name }}\"\n *ngFor=\"let datapoint of displayConfig.datapoints\"\n [ngClass]=\"{ active: datapoint.__active }\"\n >\n <i\n class=\"text-warning m-l-4\"\n c8yIcon=\"exclamation-triangle\"\n [tooltip]=\"'At least 1 data point must be active.' | translate\"\n container=\"body\"\n *ngIf=\"!hasAtLeastOneDatapointActive && datapoint.__active\"\n [adaptivePosition]=\"false\"\n ></i>\n <button\n class=\"c8y-datapoint-pill__btn\"\n title=\"{{\n (datapoint.__active ? hideDatapointLabel : showDatapointLabel) | translate\n }} \"\n type=\"button\"\n (click)=\"toggleChart(datapoint)\"\n >\n <i\n class=\"icon-14\"\n [c8yIcon]=\"datapoint.__active ? 'eye text-primary' : 'eye-slash text-muted'\"\n ></i>\n </button>\n <div class=\"c8y-datapoint-pill__label c8y-datapoint-pill__btn\">\n <i\n class=\"m-r-4 icon-14\"\n c8yIcon=\"circle\"\n [ngStyle]=\"{\n color: datapoint.color\n }\"\n ></i>\n <span\n class=\"text-truncate\"\n [ngClass]=\"{ 'text-muted': !datapoint.__active }\"\n >\n <span class=\"text-truncate\">\n {{ datapoint.label }}\n </span>\n <small class=\"text-muted text-10\">\n {{ datapoint.__target.name }}\n </small>\n </span>\n <i\n class=\"text-warning m-l-4\"\n c8yIcon=\"exclamation-triangle\"\n [tooltip]=\"\n 'Measurements received for this data point may be out of sync.' | translate\n \"\n container=\"body\"\n *ngIf=\"datapointsOutOfSync.get(datapoint)\"\n [adaptivePosition]=\"false\"\n ></i>\n </div>\n </div>\n <!-- Alarms -->\n\n <ng-container *ngFor=\"let alarm of alarms\">\n <div\n class=\"c8y-alarm-pill flex-no-shrink\"\n title=\"{{ alarm.filters.type }} \"\n *ngIf=\"alarm.__active\"\n >\n <i\n class=\"text-warning m-l-4\"\n c8yIcon=\"exclamation-triangle\"\n [tooltip]=\"\n 'Alarm of this type is currently active and outside of the selected time range'\n | translate\n \"\n container=\"body\"\n *ngIf=\"displayConfig?.activeAlarmTypesOutOfRange?.includes(alarm.filters.type)\"\n [adaptivePosition]=\"false\"\n ></i>\n <button\n class=\"c8y-alarm-pill__btn\"\n title=\"{{ alarm.filters.type }} \"\n type=\"button\"\n (click)=\"toggleAlarmEventType(alarm)\"\n >\n <i\n class=\"icon-14\"\n [c8yIcon]=\"alarm.__hidden ? 'eye-slash text-muted' : 'eye text-primary'\"\n ></i>\n </button>\n <button\n class=\"c8y-alarm-pill__label c8y-alarm-pill__btn\"\n (click)=\"toggleMarkedArea(alarm)\"\n [ngClass]=\"{\n active: !isMarkedAreaEnabled && alarm.filters.type === enabledMarkedAreaAlarmType\n }\"\n >\n <span\n class=\"circle-icon-wrapper circle-icon-wrapper--small m-r-4\"\n [style.background-color]=\"alarm.color\"\n >\n <i\n class=\"stroked-icon\"\n c8yIcon=\"bell\"\n ></i>\n </span>\n <span\n class=\"text-truncate\"\n [ngClass]=\"{ 'text-muted': alarm.__hidden }\"\n >\n <span class=\"text-truncate\">\n {{ alarm.filters.type }}\n </span>\n <small class=\"text-muted text-10\">\n {{ alarm.__target.name }}\n </small>\n </span>\n </button>\n </div>\n </ng-container>\n\n <!-- Events -->\n <ng-container *ngFor=\"let event of events\">\n <div\n class=\"c8y-event-pill flex-no-shrink\"\n title=\"{{ event.filters.type }}\"\n *ngIf=\"event.__active\"\n >\n <button\n class=\"c8y-event-pill__btn\"\n title=\"{{ event.filters.type }} \"\n type=\"button\"\n (click)=\"toggleAlarmEventType(event)\"\n >\n <i\n class=\"icon-14\"\n [c8yIcon]=\"event.__hidden ? 'eye-slash text-muted' : 'eye text-primary'\"\n ></i>\n </button>\n <div class=\"c8y-event-pill__label c8y-event-pill__btn\">\n <span\n class=\"circle-icon-wrapper circle-icon-wrapper--small m-r-4\"\n [ngStyle]=\"{ 'background-color': event.color }\"\n >\n <i\n class=\"stroked-icon\"\n c8yIcon=\"online1\"\n ></i>\n </span>\n <span\n class=\"text-truncate\"\n [ngClass]=\"{ 'text-muted': event.__hidden }\"\n >\n <span class=\"text-truncate\">\n {{ event.filters.type }}\n </span>\n <small class=\"text-muted text-10\">\n {{ event.__target.name }}\n </small>\n </span>\n </div>\n </div>\n </ng-container>\n </div>\n </div>\n </div>\n</div>\n\n<c8y-charts\n #chart\n [config]=\"displayConfig\"\n [alerts]=\"alerts\"\n (updateAlarmsAndEvents)=\"updateAlarmsAndEvents($event)\"\n (configChangeOnZoomOut)=\"updateDashboardTimeContext($event)\"\n (datapointOutOfSync)=\"handleDatapointOutOfSync($event)\"\n (timeRangeChangeOnRealtime)=\"updateTimeRangeOnRealtime($event)\"\n (isMarkedAreaEnabled)=\"isMarkedAreaEnabled = $event\"\n></c8y-charts>\n\n<ng-template #legendHelpTemplate>\n <div [innerHTML]=\"legendHelp\"></div>\n</ng-template>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":["i3","i5","i1","i2","i4","CommonModule","i6","i7","i8"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA2Ea,oCAAoC,CAAA;IAE/C,IACI,aAAa,CAAC,QAA0B,EAAA;QAC1C,IAAI,QAAQ,EAAE;AACZ,YAAA,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,QAAQ,CAAC;YAC7C;;AAEF,QAAA,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,IAAI,CAAC;;IAwC3C,WACsB,CAAA,YAAmC,EACnC,yBAAoD,EAAA;QADpD,IAAY,CAAA,YAAA,GAAZ,YAAY;QACZ,IAAyB,CAAA,yBAAA,GAAzB,yBAAyB;AAvC9B,QAAA,IAAA,CAAA,WAAW,GAAG,MAAM,CAAC,WAAW,C