UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1 lines • 84.1 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 OnChanges,\n OnDestroy,\n OnInit,\n Optional,\n signal,\n SimpleChanges,\n TemplateRef,\n ViewChild\n} from '@angular/core';\nimport { FormBuilder, NgForm, Validators } from '@angular/forms';\nimport {\n CommonModule,\n CoreModule,\n DynamicComponentAlertAggregator,\n DynamicComponentService,\n FormsModule,\n GainsightService,\n OnBeforeSave,\n WidgetTimeContextDateRangeService\n} from '@c8y/ngx-components';\nimport {\n AlarmDetails,\n AlarmEventSelectorModule,\n EventDetails,\n SelectedDatapoint\n} from '@c8y/ngx-components/alarm-event-selector';\nimport {\n ContextDashboardComponent,\n WidgetConfigComponent,\n WidgetConfigService\n} from '@c8y/ngx-components/context-dashboard';\nimport {\n DatapointAttributesFormConfig,\n DatapointSelectorModalOptions,\n DatapointSelectorModule,\n KPIDetails\n} from '@c8y/ngx-components/datapoint-selector';\nimport {\n CHART_VIEW_CONTEXT,\n ChartAlarmsService,\n ChartEventsService,\n ChartHelpersService,\n ChartsComponent,\n DatapointsGraphKPIDetails,\n DatapointsGraphWidgetConfig,\n DatapointsGraphWidgetTimeProps,\n LEGEND_DISPLAY_OPTIONS,\n PRODUCT_EXPERIENCE_DATA_EXPLORER_AND_GRAPH\n} from '@c8y/ngx-components/echart';\nimport {\n GlobalContextWidgetWrapperComponent,\n WidgetControls\n} from '@c8y/ngx-components/global-context';\nimport { defaultWidgetIds } from '@c8y/ngx-components/widgets/definitions';\nimport { PopoverModule } from 'ngx-bootstrap/popover';\nimport { TooltipModule } from 'ngx-bootstrap/tooltip';\nimport { Subject, takeUntil } from 'rxjs';\nimport { Observable } from 'rxjs/internal/Observable';\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 GlobalContextWidgetWrapperComponent\n ],\n providers: [\n ChartEventsService,\n ChartAlarmsService,\n ChartHelpersService,\n WidgetTimeContextDateRangeService\n ]\n})\nexport class DatapointsGraphWidgetConfigComponent\n implements OnInit, OnChanges, OnBeforeSave, OnDestroy\n{\n private _config = signal<DatapointsGraphWidgetConfig>({} as DatapointsGraphWidgetConfig);\n private isFirstConfigSet = true;\n\n @Input() set config(value: DatapointsGraphWidgetConfig | undefined) {\n if (!value) return;\n\n if (this.isFirstConfigSet) {\n this._config.set({ ...value });\n this.isFirstConfigSet = false;\n } else {\n this._config.set({\n ...this._config(),\n dateFrom: value.dateFrom,\n dateTo: value.dateTo,\n interval: value.interval,\n refreshInterval: value.refreshInterval,\n isAutoRefreshEnabled: value.isAutoRefreshEnabled,\n aggregation: value.aggregation,\n realtime: value.realtime,\n displayMode: value.displayMode,\n refreshOption: value.refreshOption,\n dateTimeContext: value.dateTimeContext\n });\n }\n }\n\n get config(): DatapointsGraphWidgetConfig | undefined {\n return this._config();\n }\n\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 widgetConfigService = inject(WidgetConfigService);\n private readonly chartHelpersService = inject(ChartHelpersService);\n private readonly gainsightService = inject(GainsightService);\n\n alerts: DynamicComponentAlertAggregator | undefined;\n formGroup: ReturnType<DatapointsGraphWidgetConfigComponent['initForm']>;\n datapointSelectDefaultFormOptions: Partial<DatapointAttributesFormConfig> = {\n showRange: true,\n showChart: true\n };\n datapointSelectionConfig: Partial<DatapointSelectorModalOptions> = {};\n activeDatapointsExists = false;\n alarmsOrEventsHaveNoMatchingDps = false;\n widgetControls: WidgetControls;\n chartViewContext: CHART_VIEW_CONTEXT = CHART_VIEW_CONTEXT.WIDGET_CONFIG;\n legendDisplayOptions = LEGEND_DISPLAY_OPTIONS;\n private destroy$ = new Subject<void>();\n private isInitialized = false;\n\n constructor(\n @Optional() private widgetConfig: WidgetConfigComponent,\n @Optional() private dashboardContextComponent: ContextDashboardComponent,\n private dynamicComponentService: DynamicComponentService\n ) {\n this.formGroup = this.initForm();\n }\n\n async ngOnInit() {\n this.widgetControls =\n (await this.dynamicComponentService.getById(defaultWidgetIds.DATAPOINTS_GRAPH_NEW)).data\n ?.widgetControls || {};\n\n const currentConfig = this._config();\n currentConfig.datapoints?.forEach(dp => this.assignContextFromContextDashboard(dp));\n\n this.form.form.addControl('config', this.formGroup);\n\n const alarms =\n currentConfig.alarmsEventsConfigs?.filter(ae => ae.timelineType === 'ALARM') || [];\n const events =\n currentConfig.alarmsEventsConfigs?.filter(ae => ae.timelineType === 'EVENT') || [];\n this.formGroup.patchValue({ ...currentConfig, alarms, events }, { emitEvent: false });\n this.isInitialized = true;\n\n this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(formValue => {\n const { alarms, events, datapoints } = formValue;\n\n this._config.set({\n ...this._config(),\n datapoints: datapoints || [],\n dataPointLegendDisplay: formValue.dataPointLegendDisplay,\n alarmsEventsConfigs: [...(alarms || []), ...(events || [])],\n displayMarkedLine: formValue.displayMarkedLine,\n displayMarkedPoint: formValue.displayMarkedPoint,\n mergeMatchingDatapoints: formValue.mergeMatchingDatapoints,\n forceMergeDatapoints: formValue.forceMergeDatapoints,\n showLabelAndUnit: formValue.showLabelAndUnit,\n showSlider: formValue.showSlider,\n yAxisSplitLines: formValue.yAxisSplitLines,\n xAxisSplitLines: formValue.xAxisSplitLines,\n numberOfDecimalPlaces: formValue.numberOfDecimalPlaces,\n aggregatedDatapoint: formValue.aggregatedDatapoint\n });\n\n this.setActiveDatapointsExists();\n this.checkForMatchingDatapoints();\n });\n }\n\n ngOnChanges(changes: SimpleChanges) {\n if (!changes.config || changes.config.isFirstChange() || !this.isInitialized) return;\n\n this.setActiveDatapointsExists();\n this.checkForMatchingDatapoints();\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) return false;\n\n const snapshot = { ...this._config() };\n\n for (const key in snapshot) {\n if (snapshot.hasOwnProperty(key)) {\n try {\n config[key] = snapshot[key];\n } catch (e) {\n // do nothing\n }\n }\n }\n\n if (snapshot.alarmsEventsConfigs) {\n config.alarmsEventsConfigs = [...snapshot.alarmsEventsConfigs];\n }\n if (snapshot.datapoints) {\n config.datapoints = [...snapshot.datapoints];\n }\n\n this.widgetConfigService.updateConfig(snapshot, true);\n\n const configSummaryGS = this.chartHelpersService.getConfigSummaryForGainsight(config);\n this.gainsightService.triggerEvent(\n PRODUCT_EXPERIENCE_DATA_EXPLORER_AND_GRAPH.EVENTS.DATA_EXPLORER_AND_GRAPH,\n {\n component: PRODUCT_EXPERIENCE_DATA_EXPLORER_AND_GRAPH.COMPONENTS.DATA_EXPLORER_DETAILS,\n action: PRODUCT_EXPERIENCE_DATA_EXPLORER_AND_GRAPH.ACTIONS.DATA_GRAPH_WIDGET_CONFIG,\n ...configSummaryGS\n }\n );\n\n return true;\n }\n\n updateTimeRangeOnRealtime(\n timeRange: Pick<DatapointsGraphWidgetConfig, 'dateFrom' | 'dateTo'>\n ): void {\n const current = this._config();\n this._config.set({\n ...current,\n ...timeRange,\n interval: current.interval || 'hours'\n });\n }\n\n updateDashboardTimeContext(timeProps: DatapointsGraphWidgetTimeProps): void {\n const dateTimeContext = {\n dateFrom: new Date(timeProps.dateFrom).toISOString(),\n dateTo: new Date(timeProps.dateTo).toISOString(),\n interval: timeProps.interval\n };\n this._config.set({\n ...this._config(),\n dateTimeContext\n });\n }\n\n updateAggregatedSliderDatapoint(selectedDatapoint: SelectedDatapoint | null): void {\n const aggregatedDatapoint = this.chartHelpersService.findMatchingDatapoint(\n this._config().datapoints || [],\n selectedDatapoint\n ) as DatapointsGraphKPIDetails | undefined;\n\n this._config.set({\n ...this._config(),\n aggregatedDatapoint\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 config = this._config();\n const alarmsEventsConfigs = config.alarmsEventsConfigs || [];\n const datapoints = config.datapoints || [];\n\n const allMatch = alarmsEventsConfigs.every(ae =>\n datapoints.some(dp => dp.__target?.id === ae.__target?.id)\n );\n\n queueMicrotask(() => {\n this.alarmsOrEventsHaveNoMatchingDps = !allMatch;\n });\n }\n\n private initForm() {\n const initialState = this._config();\n\n const form = this.formBuilder.group({\n datapoints: [\n [] as DatapointsGraphKPIDetails[],\n [Validators.required, Validators.minLength(1)]\n ],\n dataPointLegendDisplay: ['auto', []],\n alarms: [[] as AlarmDetails[]],\n events: [[] as EventDetails[]],\n displayMarkedLine: [true, []],\n displayMarkedPoint: [true, []],\n mergeMatchingDatapoints: [true, []],\n forceMergeDatapoints: [false, []],\n showLabelAndUnit: [true, []],\n displayAggregationSelection: [false, []],\n canDecoupleGlobalTimeContext: [false, []],\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 aggregatedDatapoint: [initialState?.aggregatedDatapoint || null]\n });\n return form;\n }\n\n private setActiveDatapointsExists() {\n const datapoints = this._config().datapoints || [];\n this.activeDatapointsExists = datapoints.filter(dp => dp.__active).length > 0;\n }\n}\n","<form [formGroup]=\"formGroup\">\n <c8y-datapoint-selection-list\n class=\"bg-component 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=\"bg-component 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 <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 datapoints 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 </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 </c8y-form-group>\n </fieldset>\n</form>\n\n<ng-template #dataPointsGraphPreview>\n @if (widgetControls) {\n <c8y-global-context-widget-wrapper\n [config]=\"config\"\n [displayMode]=\"'preview'\"\n [widgetControls]=\"widgetControls\"\n ></c8y-global-context-widget-wrapper>\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 translate>\n Find out more in the\n <a c8y-guide-href=\"/docs/cockpit/widgets-collection/#data-point-graph\">\n user documentation\n </a>\n .\n </small>\n </p>\n </c8y-ui-empty-state>\n }\n</ng-template>\n","import { A11yModule } from '@angular/cdk/a11y';\nimport { CommonModule } from '@angular/common';\nimport { ChangeDetectorRef, Component, Input, OnDestroy, Optional, ViewChild } from '@angular/core';\nimport { ALARM_STATUS_LABELS, AlarmStatusType, SeveritySettings } from '@c8y/client';\nimport {\n AGGREGATION_ICONS,\n AGGREGATION_TEXTS,\n CoreModule,\n DynamicComponentAlertAggregator,\n DynamicComponentService,\n SelectableItem,\n SelectComponent,\n SelectItemDirective,\n SelectedItemsDirective,\n WidgetTimeContextDateRangeService\n} from '@c8y/ngx-components';\nimport { AlarmsModule } from '@c8y/ngx-components/alarms';\nimport { ContextDashboardComponent } from '@c8y/ngx-components/context-dashboard';\nimport type { KPIDetails } from '@c8y/ngx-components/datapoint-selector';\nimport {\n AlarmDetailsExtended,\n AlarmOrEventExtended,\n CHART_VIEW_CONTEXT,\n ChartAlarmsService,\n ChartEventsService,\n ChartsComponent,\n DatapointsGraphKPIDetails,\n DatapointsGraphWidgetConfig,\n DatapointsGraphWidgetTimeProps,\n EventDetailsExtended,\n SelectableDatapoint,\n SeverityType\n} from '@c8y/ngx-components/echart';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport {\n GLOBAL_CONTEXT_DISPLAY_MODE,\n GLOBAL_CONTEXT_EVENTS,\n GLOBAL_CONTEXT_SOURCE,\n GlobalContextEvent,\n GlobalContextState,\n GlobalContextWidgetWrapperComponent,\n REFRESH_OPTION,\n WidgetConfigMigrationService,\n WidgetControls\n} from '@c8y/ngx-components/global-context';\nimport { defaultWidgetIds } from '@c8y/ngx-components/widgets/definitions';\nimport { TranslateService } from '@ngx-translate/core';\nimport { cloneDeep, merge } from 'lodash-es';\nimport { BsDropdownModule } from 'ngx-bootstrap/dropdown';\nimport { PopoverModule } from 'ngx-bootstrap/popover';\nimport { TooltipModule } from 'ngx-bootstrap/tooltip';\nimport { BehaviorSubject, Subject } from 'rxjs';\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 GlobalContextWidgetWrapperComponent,\n SelectComponent,\n SelectItemDirective,\n SelectedItemsDirective\n ],\n providers: [ChartEventsService, ChartAlarmsService, WidgetTimeContextDateRangeService]\n})\nexport class DatapointsGraphWidgetViewComponent implements 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 hasAtLeastOneDatapointActive = true;\n hasAtLeastOneAlarmActive = true;\n isMarkedAreaEnabled = false;\n loadedDatapoints: DatapointsGraphKPIDetails[] = [];\n loadedAlarmsOrEvents: AlarmOrEventExtended[] = [];\n isLoading$ = new BehaviorSubject<boolean>(false);\n isSliderBeingDragged$ = new BehaviorSubject<boolean>(false);\n /*\n * @description: The type of alarm that has marked area enabled.\n */\n enabledMarkedAreaAlarmType: string | undefined;\n chartViewContext = CHART_VIEW_CONTEXT.WIDGET_VIEW;\n\n currentConfig: Partial<GlobalContextState>;\n widgetControls: WidgetControls;\n globalContextState: Partial<DatapointsGraphWidgetConfig> & Partial<GlobalContextState> = null;\n /** Combined number of datapoints, alarms, and events */\n totalLegendItems = 0;\n /** Selectable items for the datapoints, alarms, events dropdown */\n selectableItems: SelectableItem<SelectableDatapoint>[] = [];\n /** Selected items from the dropdown */\n selectedItems: SelectableItem<SelectableDatapoint>[] = [];\n\n @Input() set config(value: DatapointsGraphWidgetConfig) {\n // Special case: sync the config with migrated values.\n // This is needed to reflect migrated states in the widget config.\n const migratedConfig = this.widgetConfigMigrationService.migrateWidgetConfig(value);\n const newConfig = merge(value, migratedConfig);\n this.displayConfig = cloneDeep(newConfig);\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(GlobalContextWidgetWrapperComponent) wrapper?: GlobalContextWidgetWrapperComponent;\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 private readonly widgetInstanceId = crypto.randomUUID();\n\n constructor(\n private translate: TranslateService,\n @Optional() private dashboardContextComponent: ContextDashboardComponent,\n private dynamicComponentService: DynamicComponentService,\n private widgetConfigMigrationService: WidgetConfigMigrationService,\n private widgetTimeContextDateRangeService: WidgetTimeContextDateRangeService,\n private cdr: ChangeDetectorRef\n ) {}\n\n async ngOnInit() {\n this.displayConfig?.datapoints?.forEach(dp => this.assignContextFromContextDashboard(dp));\n this.displayConfig.isRealtimeEnabled = this.displayConfig.realtime;\n\n this.totalLegendItems =\n (this.displayConfig?.datapoints?.length || 0) +\n (this.displayConfig?.alarmsEventsConfigs?.length || 0);\n\n this.loadedDatapoints = (this.displayConfig?.datapoints || []).filter(dp => dp.__active);\n this.loadedAlarmsOrEvents = (this.displayConfig?.alarmsEventsConfigs || []).filter(\n aOrE => aOrE.__active && !aOrE.__hidden\n );\n if (this.totalLegendItems > 5 || this.displayConfig?.dataPointLegendDisplay === 'dropdown') {\n this.selectableItems = this.buildSelectableItems();\n // Initialize selectedItems with the currently active items\n this.selectedItems = this.selectableItems.filter(item => {\n if (item.value.type === 'DATAPOINT') {\n return !!item.value.original.__active;\n } else {\n return item.value.original.__active && !item.value.original.__hidden;\n }\n });\n }\n\n this.widgetControls =\n (await this.dynamicComponentService.getById(defaultWidgetIds.DATAPOINTS_GRAPH_NEW)).data\n ?.widgetControls || {};\n }\n\n onGlobalContextChange(event: GlobalContextEvent) {\n const { context, diff } = event;\n const { dateTimeContext } = context;\n const { dateFrom, dateTo, interval } = dateTimeContext;\n\n const isRealtimeEnabled =\n context.refreshOption === REFRESH_OPTION.LIVE &&\n context.isAutoRefreshEnabled &&\n context.refreshInterval === 5000;\n\n if (this.displayConfig.displayMode !== GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD) {\n this.displayConfig = {\n ...this.displayConfig,\n ...structuredClone(context),\n dateFrom: new Date(dateFrom),\n dateTo: new Date(dateTo),\n interval\n };\n\n this.globalContextState = {\n ...this.displayConfig,\n realtime: isRealtimeEnabled,\n dateTimeContext: { ...this.displayConfig.dateTimeContext }\n };\n\n if (this.isSliderBeingDragged$.value === true) {\n this.isSliderBeingDragged$.next(false);\n return;\n }\n\n this.widgetTimeContextDateRangeService.updateInitialTimeRange(null);\n return;\n }\n\n if (this.displayConfig?.realtime !== isRealtimeEnabled) {\n // Sync dates to avoid jumps in the chart\n this.displayConfig = merge(this.displayConfig, {\n dateTimeContext: structuredClone(context),\n dateFrom: new Date(dateFrom),\n dateTo: new Date(dateTo),\n interval\n });\n\n // Update global context state when realtime state changes\n this.globalContextState = merge({}, this.globalContextState, this.displayConfig);\n\n this.displayConfig = {\n ...this.displayConfig,\n realtime: isRealtimeEnabled\n };\n\n this.globalContextState = { ...this.globalContextState, realtime: isRealtimeEnabled };\n }\n\n // Realtime is a special case, we need to block \"automatic\" emissions from the global context\n // GC in auto mode emits every 5 seconds. We only want to react to user changes.\n if (\n isRealtimeEnabled &&\n diff?.dateTimeContext &&\n Object.keys(diff).length === 1 &&\n Object.keys(diff.dateTimeContext).length === 2\n ) {\n // Sync dates to avoid jumps in the chart\n this.displayConfig = merge(this.displayConfig, {\n dateTimeContext: structuredClone(context),\n dateFrom: new Date(dateFrom),\n dateTo: new Date(dateTo),\n interval\n });\n\n this.globalContextState = merge({}, this.globalContextState, this.displayConfig);\n\n return;\n }\n\n this.displayConfig = {\n ...this.displayConfig,\n ...structuredClone(context),\n dateFrom: new Date(dateFrom),\n dateTo: new Date(dateTo),\n interval\n };\n\n this.globalContextState = {\n ...this.displayConfig,\n dateTimeContext: { ...this.displayConfig.dateTimeContext }\n };\n\n if (\n context.source === GLOBAL_CONTEXT_SOURCE.DASHBOARD &&\n this.displayConfig.displayMode === GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD\n ) {\n this.widgetTimeContextDateRangeService.updateInitialTimeRange(null);\n }\n }\n\n ngOnDestroy() {\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n updateDashboardTimeContext(timeProps: DatapointsGraphWidgetTimeProps): void {\n const chartOptions = this.chartComponent?.echartsInstance?.getOption();\n const isEndZoomChanged = chartOptions?.dataZoom[0]?.end !== 100;\n\n if (this.displayConfig.displayMode === GLOBAL_CONTEXT_DISPLAY_MODE.DASHBOARD) {\n const dateTimeContext = isEndZoomChanged\n ? {\n dateFrom: new Date(timeProps.dateFrom).toISOString(),\n dateTo: new Date(timeProps.dateTo).toISOString(),\n interval: timeProps.interval\n }\n : { dateFrom: new Date(timeProps.dateFrom).toISOString(), interval: timeProps.interval };\n\n window.dispatchEvent(\n new CustomEvent(GLOBAL_CONTEXT_EVENTS.UPDATE_GLOBAL_CONTEXT_HISTORY, {\n detail: {\n type: GLOBAL_CONTEXT_EVENTS.UPDATE_GLOBAL_CONTEXT_HISTORY,\n payload: {\n dateTimeContext,\n isAutoRefreshEnabled: false,\n aggregation: this.displayConfig.aggregation,\n source: GLOBAL_CONTEXT_SOURCE.WIDGET,\n eventSourceId: this.widgetInstanceId\n },\n timestamp: Date.now()\n }\n })\n );\n } else {\n this.isSliderBeingDragged$.next(true);\n const dateTimeContext = {\n dateFrom: new Date(timeProps.dateFrom),\n dateTo: new Date(timeProps.dateTo),\n interval: timeProps.interval\n };\n\n // Update displayConfig for internal state\n this.displayConfig = {\n ...this.displayConfig,\n dateTimeContext: structuredClone(dateTimeContext) as any,\n dateFrom: new Date(timeProps.dateFrom),\n dateTo: new Date(timeProps.dateTo),\n interval: timeProps.interval,\n isAutoRefreshEnabled: false\n };\n\n this.globalContextState = {\n ...this.displayConfig,\n dateTimeContext: { ...dateTimeContext } as any\n };\n\n // Trigger wrapper to update its internal state\n this.wrapper.pauseAutoRefresh();\n this.wrapper.updateDateTimeContext(dateTimeContext);\n }\n }\n\n buildSelectableItems(): SelectableItem<SelectableDatapoint>[] {\n const dpItems: SelectableItem<SelectableDatapoint>[] = (\n this.displayConfig.datapoints || []\n ).map(dp => {\n return {\n label: dp.label,\n value: {\n type: 'DATAPOINT',\n original: dp\n }\n };\n });\n\n const alarmItems: SelectableItem<SelectableDatapoint>[] = (\n this.displayConfig.alarmsEventsConfigs || []\n )\n .filter(ae => ae.timelineType === 'ALARM')\n .map(alarm => {\n return {\n label: alarm.label,\n value: {\n type: 'ALARM',\n original: alarm\n }\n };\n });\n\n const eventItems: SelectableItem<SelectableDatapoint>[] = (\n this.displayConfig.alarmsEventsConfigs || []\n )\n .filter(ae => ae.timelineType === 'EVENT')\n .map(event => {\n return {\n label: event.label,\n value: {\n type: 'EVENT',\n original: event\n }\n };\n });\n\n return [...dpItems, ...alarmItems, ...eventItems];\n }\n\n onItemSelected(item: SelectableItem<SelectableDatapoint>): void {\n if (item.value.type === 'DATAPOINT') {\n this.toggleChart(item.value.original as DatapointsGraphKPIDetails);\n } else {\n this.toggleAlarmEventType(item.value.original as AlarmOrEventExtended);\n }\n }\n\n onItemDeselected(item: SelectableItem<SelectableDatapoint>): void {\n if (item.value.type === 'DATAPOINT') {\n this.toggleChart(item.value.original as DatapointsGraphKPIDetails);\n } else {\n this.toggleAlarmEventType(item.value.original as AlarmOrEventExtended);\n }\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 this.updateSelectedItems();\n return;\n }\n datapoint.__active = !datapoint.__active;\n this.hasAtLeastOneDatapointActive = true;\n\n this.updateSelectedItems();\n\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\n this.updateSelectedItems();\n\n if (!this.loadedAlarmsOrEvents.find(aOrE => aOrE.filters.type === alarmOrEvent.filters.type)) {\n this.loadedAlarmsOrEvents.push(alarmOrEvent);\n this.displayConfig.alarmsEventsConfigs.map(ae => {\n if (ae.filters.type === alarmOrEvent.filters.type) {\n ae.__active = !ae.__active;\n ae.__hidden = !alarmOrEvent.__hidden;\n }\n });\n this.updateSelectedItems();\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 isLastActiveDatapoint(selectedItem: SelectableItem<SelectableDatapoint>): boolean {\n if (!selectedItem) {\n return false;\n }\n\n if (!selectedItem?.value || selectedItem.value.type !== 'DATAPOINT') {\n return false;\n }\n // Check if this is the last active datapoint\n return !this.hasAtLeastOneDatapointActive && selectedItem.value.original.__active;\n }\n\n private updateSelectedItems(): void {\n const filtered = this.selectableItems.filter(item => {\n if (item.value.type === 'DATAPOINT') {\n return !!item.value.original.__active;\n } else {\n return item.value.original.__active && !item.value.original.__hidden;\n }\n });\n this.selectedItems = [...filtered];\n this.cdr.detectChanges();\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","@if (widgetControls) {\n <c8y-global-context-widget-wrapper\n [widgetControls]=\"widgetControls\"\n [isLoading]=\"isLoading$ | async\"\n [config]=\"displayConfig\"\n (globalContextChange)=\"onGlobalContextChange($event)\"\n ></c8y-global-context-widget-wrapper>\n}\n<div\n class=\"p-l-16 p-r-16\"\n style=\"min-height: 34px\"\n>\n <div class=\"d-flex gap-16 a-i-center\">\n @if (hasAtLeastOneAlarmActive) {\n <c8y-alarms-filter\n class=\"d-contents form-group-sm min-width-0\"\n (onFilterApplied)=\"filterSeverity($event)\"\n ></c8y-alarms-filter>\n }\n @if (displayConfig?.datapoints.length > 0) {\n <div class=\"d-flex a-i-center min-width-0\">\n @if (\n displayConfig.dataPointLegendDisplay === 'dropdown' ||\n (displayConfig.dataPointLegendDisplay === 'auto' && totalLegendItems > 5)\n ) {\n <c8y-select\n class=\"min-width-0 c8y-select-v2--sm\"\n aria-label=\"Select datapoints, alarms or events\"\n [placeholder]=\"'Select datapoints, alarms or events' | translate\"\n [multi]=\"true\"\n [filterItems]=\"true\"\n [(ngModel)]=\"selectedItems\"\n (onSelect)=\"onItemSelected($event)\"\n (onDeselect)=\"onItemDeselected($event)\"\n >\n @for (item of selectableItems; track item.value) {\n <div\n class=\"d-flex a-i-center gap-4\"\n *c8ySelectItem=\"item.value; label: item.label\"\n >\n @if (\n !hasAtLeastOneDatapointActive &&\n item.value.type === 'DATAPOINT' &&\n item.value.original.__active\n ) {\n <i\n class=\"text-warning m-r-4\"\n c8yIcon=\"exclamation-triangle\"\n [tooltip]=\"'At least 1 data point must be active.' | translate\"\n container=\"body\"\n data-cy=\"datapoint-warning-icon\"\n [adaptivePosition]=\"false\"\n ></i>\n }\n @if (item.value.type === 'DATAPOINT') {\n <span\n class=\"circle-icon-wrapper a-s-start circle-icon-wrapper--small\"\n [style.background-color]=\"item.value.original.color\"\n ></span>\n }\n @if (item.value.type === 'ALARM') {\n <span\n class=\"circle-icon-wrapper a-s-start circle-icon-wrapper--small\"\n [style.background-color]=\"item.value.original.color\"\n >\n <i\n class=\"stroked-icon\"\n c8yIcon=\"bell\"\n ></i>\n </span>\n }\n @if (item.value.type === 'EVENT') {\n <span\n class=\"circle-icon-wrapper a-s-start circle-icon-wrapper--small\"\n [ngStyle]=\"{ 'background-color': item.value.original.color }\"\n >\n <i\n class=\"stroked-icon\"\n c8yIcon=\"online1\"\n ></i>\n </span>\n }\n <span class=\"text-truncate d-col\">\n <small class=\"text-truncate\">{{ item.value.original.label }}</small>\n <span class=\"text-muted text-10\">{{ item.value.original.__target?.name }}</span>\n </span>\n </div>\n }\n <span\n class=\"tag tag--info chip\"\n title=\"{{ selectedItem.label }}\"\n *c8ySelectedItems=\"let selectedItem\"\n >\n <button\n class=\"btn btn-xs btn-clean text-10\"\n title=\"{{ selectedItem.label }}\"\n type=\"button\"\n (click)=\"\n $event.preventDefault(); $event.stopPropagation(); onItemDeselected(selectedItem)\n \"\n >\n <i c8yIcon=\"times\"></i>\n </button>\n @if (isLastActiveDatapoint(selectedItem)) {\n <i\n class=\"text-warning a-s-start\"\n c8yIcon=\"exclamation-triangle\"\n [tooltip]=\"'At least 1 data point must be active.' | translate\"\n container=\"body\"\n data-cy=\"datapoint-warning-icon\"\n [adaptivePosition]=\"false\"\n ></i>\n }\n @if (selectedItem.value.type === 'DATAPOINT') {\n <span\n class=\"circle-icon-wrapper circle-icon-wrapper--small\"\n [style.background-color]=\"selectedItem.value?.original.color || ''\"\n ></span>\n }\n @if (selectedItem.value?.type === 'ALARM') {\n <span\n class=\"circle-icon-wrapper circle-icon-wrapper--small\"\n [style.background-color]=\"selectedItem.value?.original.color || ''\"\n >\n <i\n class=\"stroked-icon\"\n c8yIcon=\"bell\"\n ></i>\n </span>\n }\n @if (selectedItem.value?.type === 'EVENT') {\n <span\n class=\"circle-icon-wrapper circle-icon-wrapper--small\"\n [ngStyle]=\"{\n 'background-color': selectedItem.value?.original.color || ''\n }\"\n >\n <i\n class=\"stroked-icon\"\n c8yIcon=\"online1\"\n ></i>\n </span>\n }\n </span>\n </c8y-select>\n } @else {\n <div class=\"inner-scroll\">\n <div class=\"p-t-4 d-flex a-i-start gap-8\">\n @for (datapoint of displayConfig.datapoints; track datapoint) {\n <div\n class=\"c8y-datapoint-pill pill--sm flex-no-shrink\"\n title=\"{{ datapoint.label }} - {{ datapoint.__target.name }}\"\n [ngClass]=\"{ active: datapoint.__active }\"\n >\n @if (!hasAtLeastOneDatapointActive && datapoint.__active) {\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 data-cy=\"datapoint-warning-icon\"\n [adaptivePosition]=\"false\"\n ></i>\n }\n <button\n class=\"c8y-datapoint-pill__btn\"\n title=\"{{\n (datapoint.__active ? hideDatapointLabel : showDatapointLabel) | translate\n }} \"\n type=\"button\"\n data-cy=\"datapoint-toggle-visibility-btn\"\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 text-bold\">\n {{ datapoint.label }}\n </span>\n <small class=\"text-muted text-10\">\n {{ datapoint.__target.name }}\n </small>\n </span>\n @if (datapointsOutOfSync.get(datapoint)) {\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.'\n | translate\n \"\n container=\"body\"\n [adaptivePosition]=\"false\"\n ></i>\n }\n </div>\n </div>\n }\n\n @for (alarm of alarms; track alarm) {\n <div\n class=\"c8y-alarm-pill pill--sm flex-no-shrink\"\n title=\"{{ alarm.filters.type }} \"\n >\n @if (displayConfig?.activeAlarmTypesOutOfRange?.includes(alarm.filters.type)) {\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