UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

966 lines (956 loc) 223 kB
import * as i0 from '@angular/core'; import { Injectable, Pipe, InjectionToken, Component, Optional, Input, EventEmitter, Output, ViewChild, forwardRef, NgModule } from '@angular/core'; import { combineLatest, Subject, BehaviorSubject, firstValueFrom, of, from, forkJoin, EMPTY, Observable, pipe, fromEvent, take, takeUntil as takeUntil$1, map as map$1 } from 'rxjs'; import { filter, map, switchMap, startWith, takeUntil, catchError, finalize, tap, debounceTime, distinctUntilChanged, shareReplay, throttleTime } from 'rxjs/operators'; import * as i3 from '@c8y/ngx-components'; import { Permissions, gettext, ViewContext, SupportedApps, CountdownIntervalComponent, DynamicComponentAlertAggregator, DynamicComponentAlert, AlarmWithChildrenRealtimeService, ContextRouteComponent, ContextRouteGuard, RouterTabsResolver, hookNavigator, hookRoute, CommonModule, CoreModule, HeaderModule, DynamicComponentModule, RelativeTimePipe } from '@c8y/ngx-components'; import { sortBy, cloneDeep } from 'lodash-es'; import * as i2 from '@c8y/client'; import { AlarmStatus, Severity, ALARM_STATUS_LABELS, SEVERITY_LABELS } from '@c8y/client'; import * as i1 from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core'; import * as i9 from '@c8y/ngx-components/interval-picker'; import { INTERVAL_TITLES, INTERVALS, IntervalPickerComponent } from '@c8y/ngx-components/interval-picker'; import * as i7 from '@angular/common'; import { DatePipe, TitleCasePipe } from '@angular/common'; import * as i8 from 'ngx-bootstrap/popover'; import { PopoverModule } from 'ngx-bootstrap/popover'; import * as i1$1 from '@angular/router'; import { NavigationEnd, RouterModule } from '@angular/router'; import * as i1$2 from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import * as i6 from 'ngx-bootstrap/dropdown'; import { BsDropdownDirective, BsDropdownModule } from 'ngx-bootstrap/dropdown'; import * as i5 from '@angular/cdk/a11y'; import { A11yModule } from '@angular/cdk/a11y'; import * as i5$1 from 'ngx-bootstrap/tooltip'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; import * as i1$3 from '@c8y/ngx-components/alarm-event-selector'; import { AlarmEventSelectorModule } from '@c8y/ngx-components/alarm-event-selector'; /** * A service to retrieve custom buttons for the alarm details view. */ class AlarmDetailsButtonService { constructor(serviceRegistry, pluginsResolver) { this.serviceRegistry = serviceRegistry; this.pluginsResolver = pluginsResolver; } get$(alarm, source) { const providers$ = this.pluginsResolver.allPluginsLoaded$.pipe(filter(Boolean), map(() => { return this.serviceRegistry.get('alarmDetailsButton'); })); return providers$.pipe(switchMap(providers => { const observables$ = providers.map(provider => provider.getAlarmDetailsButton$(alarm, source).pipe(startWith(false))); return combineLatest(observables$); }), map(indicators => { return indicators.filter(Boolean); }), map(indicators => sortBy(indicators, this.byPriority))); } byPriority(item) { if (item.priority === undefined) { return 0; } return -item.priority; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlarmDetailsButtonService, deps: [{ token: i3.ServiceRegistry }, { token: i3.PluginsResolveService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlarmDetailsButtonService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlarmDetailsButtonService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i3.ServiceRegistry }, { type: i3.PluginsResolveService }] }); /** * A pipe to provide custom buttons for the alarm details view. * * Will call `get$()` method of `AlarmDetailsButtonService` to get the custom buttons for the provided alarm. */ class AlarmDetailsButtonPipe { constructor(alarmDetailsButtonService) { this.alarmDetailsButtonService = alarmDetailsButtonService; } transform(alarm, source) { return this.alarmDetailsButtonService.get$(alarm, source); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlarmDetailsButtonPipe, deps: [{ token: AlarmDetailsButtonService }], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "18.2.13", ngImport: i0, type: AlarmDetailsButtonPipe, isStandalone: true, name: "alarmDetailsButton" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlarmDetailsButtonPipe, decorators: [{ type: Pipe, args: [{ standalone: true, name: 'alarmDetailsButton', pure: true }] }], ctorParameters: () => [{ type: AlarmDetailsButtonService }] }); class AlarmDetailsService { constructor(permissions) { this.permissions = permissions; this.STATUS_ATTRIBUTE = 'status'; } /** * Retrieves the username of the user who acknowledged an alarm status. * * This method checks if the provided status is equal to the acknowledged * status. If it is not, or if the * audit log is empty or the first log item does not contain a user, the * method returns a default value ('--'). * * If the status is the acknowledged status and the audit log contains valid * records, the method iterates over the audit records in reverse order * (starting from the most recent). It finds the first record where the * status attribute (defined by this.STATUS_ATTRIBUTE) has been changed to * the acknowledged status. The method then returns the username of the user * who made this change. * * If no such change is found in the audit records, it returns the username * from the first record of the audit log. * * There can be multiple audit logs with ACKNOWLEDGED status. * * @param status - The current status of the alarm. * @param auditLog - An array of audit records to process. * @returns The username of the user who acknowledged the status * or '--' if the status is not acknowledged or audit log is invalid. */ getAcknowledgedBy(status, auditLog) { let acknowledgedBy = '--'; if (status !== AlarmStatus.ACKNOWLEDGED || !auditLog || !auditLog[0]?.user) { return acknowledgedBy; } acknowledgedBy = auditLog[0].user; return auditLog.reduceRight((acc, auditLogItem) => { const changes = Array.from(auditLogItem.changes || []); const acknowledgedStatusChange = changes.find((change) => change.attribute === this.STATUS_ATTRIBUTE && change.newValue === AlarmStatus.ACKNOWLEDGED); return (acknowledgedStatusChange && auditLogItem.user) || acc; }, acknowledgedBy); } /** * Calculates the acknowledge time from a list of audit records. * * This method iterates over the provided audit records in reverse order * (starting from the most recent) and finds the first record where a * specific status attribute (defined by this.STATUS_ATTRIBUTE) has been * acknowledged. It then returns the creation time of that record. * * If no such record is found, the method returns the creation time of the * first audit record. If the audit record list is empty, it returns null. * * There can be multiple audit logs with ACKNOWLEDGED status. * * @param auditLog - An array of audit records to process. * @returns The creation time of the acknowledged record, * the creation time of the first record if no acknowledged record is found, * or null if the audit log is empty. */ getAcknowledgeTime(auditLog) { const initialValue = auditLog.length ? auditLog[0].creationTime : null; return auditLog.reduceRight((acc, auditLogItem) => { const changes = Array.from(auditLogItem.changes || []); const acknowledgedStatusChange = changes.find((change) => change.attribute === this.STATUS_ATTRIBUTE && change.newValue === AlarmStatus.ACKNOWLEDGED); return acknowledgedStatusChange ? auditLogItem.creationTime : acc; }, initialValue); } /** * Retrieves the end time of an event from an audit log. * * The method processes the provided audit log to find the first instance * (starting from the most recent record) where the status was changed to 'CLEARED'. * It iterates over the audit records and * checks the changes in each record to find this status change. * * If a record with the CLEARED status is found, the method returns the creation time * of that record. If the entire audit log is processed without finding a CLEARED status, * the creation time of the first audit log record is returned. * * If the audit log is empty or null, the method returns null. * * There can be only one audit log with CLEARED status. * * @param auditLog - An array of audit records to process. * @returns The creation time of the record with the CLEARED status, * the creation time of the first record if no CLEARED status is found, * or null if the audit log is empty or null. */ getEndTime(auditLog) { if (!auditLog || auditLog.length === 0) { return null; } let latestClearedAuditTime = null; for (const auditLogItem of auditLog) { const changes = Array.from(auditLogItem.changes || []); const clearedStatusChange = changes.find(change => change.attribute === this.STATUS_ATTRIBUTE && change.newValue === AlarmStatus.CLEARED); if (clearedStatusChange) { if (!latestClearedAuditTime || auditLogItem.creationTime > latestClearedAuditTime) { latestClearedAuditTime = auditLogItem.creationTime; } } } return latestClearedAuditTime || auditLog[0].creationTime; } checkIfHasAnyRoleAllowingToCreateSmartRule() { const ROLES_ALLOWING_SMART_RULE_CREATION = [ [ Permissions.ROLE_INVENTORY_ADMIN, Permissions.ROLE_INVENTORY_CREATE, Permissions.ROLE_MANAGED_OBJECT_ADMIN, Permissions.ROLE_MANAGED_OBJECT_CREATE ], [Permissions.ROLE_CEP_MANAGEMENT_ADMIN, Permissions.ROLE_SMARTRULE_ADMIN] ]; return (this.permissions.hasAnyRole(ROLES_ALLOWING_SMART_RULE_CREATION[0]) && this.permissions.hasAnyRole(ROLES_ALLOWING_SMART_RULE_CREATION[1])); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlarmDetailsService, deps: [{ token: i3.Permissions }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlarmDetailsService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlarmDetailsService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i3.Permissions }] }); const ALARMS_MODULE_CONFIG = new InjectionToken('AlarmsModuleConfig'); const ALARM_STATUS_ICON = { ALERT_IDLE: 'c8y-alert-idle', BELL_SLASH: 'bell-slash', BELL: 'bell' }; /** * A lookup table to map alarm statuses to corresponding icons. */ const AlarmIconMap = { [AlarmStatus.CLEARED]: ALARM_STATUS_ICON.ALERT_IDLE, [AlarmStatus.ACKNOWLEDGED]: ALARM_STATUS_ICON.BELL_SLASH, [AlarmStatus.ACTIVE]: ALARM_STATUS_ICON.BELL }; const ALARM_SEVERITY_ICON = { CIRCLE: 'circle', HIGH_PRIORITY: 'high-priority', WARNING: 'warning', EXCLAMATION_CIRCLE: 'exclamation-circle' }; const HELP_ICON = 'help'; /** * A lookup table to map alarm severity types to corresponding icons. */ const ALARM_SEVERITY_ICON_MAP = { [Severity.CRITICAL]: ALARM_SEVERITY_ICON.EXCLAMATION_CIRCLE, [Severity.MAJOR]: ALARM_SEVERITY_ICON.WARNING, [Severity.MINOR]: ALARM_SEVERITY_ICON.HIGH_PRIORITY, [Severity.WARNING]: ALARM_SEVERITY_ICON.CIRCLE }; /** * Extended interval titles with an additional title for the case when no date is selected. */ const INTERVAL_TITLES_EXTENDED = { ...INTERVAL_TITLES, none: gettext('No date filter') }; const INTERVALS_EXTENDED = [ { id: 'none', title: gettext('No date filter') }, ...INTERVALS ]; const DEFAULT_ALARM_COUNTS = { CRITICAL: 0, MAJOR: 0, MINOR: 0, WARNING: 0 }; const DEFAULT_SEVERITY_VALUES = { [Severity.CRITICAL]: true, [Severity.MAJOR]: true, [Severity.MINOR]: true, [Severity.WARNING]: true }; const DEFAULT_STATUS_VALUES = { [AlarmStatus.ACTIVE]: true, [AlarmStatus.ACKNOWLEDGED]: true, [AlarmStatus.CLEARED]: true }; const ALARMS_PATH = 'alarms'; /** * Default properties of a alarm. Used to extract the custom properties from a Alarm object. */ const ALARM_DEFAULT_PROPERTIES = [ 'severity', 'source', 'type', 'time', 'text', 'id', 'status', 'count', 'name', 'history', 'self', 'creationTime', 'firstOccurrenceTime', 'lastUpdated' ]; const THROTTLE_REALTIME_REFRESH = 1_000; /** * This service is a duplicate of smart-rules-service with slight name change. * Duplicating allows to pass 'Verify App tutorial' job. * Name renames allows to pass 'Reusable build codex' job. * Overall this service is considered as a workaround. * In ticket MTM-58985 we will investigate if it's possible to remove this service * along with making failing jobs pass. */ class Ng1SmartRulesUpgradeService { } function SmartRulesUpgradeServiceFactory(injector) { return injector.get('smartRulesSvc'); } const smartRulesUpgradeServiceProvider = { provide: Ng1SmartRulesUpgradeService, useFactory: SmartRulesUpgradeServiceFactory, deps: ['$injector'] }; /** * Service for managing and retrieving alarms data within the alarms view. * * The `AlarmsViewService` provides functionality to interact with alarms, * including filtering, counting, and translation-related operations in an alarms view. * * This service relies on the `AlarmService` for fetching alarm data and the `OptionsService` * for configuring alarms view options. */ class AlarmsViewService { constructor(alarmService, optionsService) { this.alarmService = alarmService; this.optionsService = optionsService; this.ALARM_REFRESH_TYPE_KEY = 'alarmsRefreshType'; this.DEFAULT_INTERVAL_VALUE = 30_000; this.DEFAULT_REFRESH_OPTION_VALUE = 'interval'; this.DEFAULT_INTERVAL_VALUES = [5_000, 10_000, 15_000, 30_000, 60_000]; this.REALTIME_UPDATE_ALARMS_MESSAGE = gettext('The list was updated, click to refresh.'); this.reloadAlarmsList$ = new Subject(); if (this.isIntervalRefresh()) { this._isIntervalEnabled = new BehaviorSubject(true); this.isIntervalEnabled$ = this._isIntervalEnabled.asObservable(); } } /** * Emits a subject to initialize the alarms reloading. */ updateAlarmList() { this.reloadAlarmsList$.next(); } /** * Retrieves a list of alarms filtered by specified severities and other optional query filters. * * @param severities an array of severities to filter the alarms. * @param showCleared flag indicating whether to show cleared alarms. Defaults to false. * @param selectedDates an array of two dates to filter alarms by creation and last update dates. * @param filter additional query filters for retrieving alarms. * * @returns A promise that resolves to a list of alarms satisfying the specified filters. */ retrieveFilteredAlarms(severities, showCleared = false, selectedDates, filter) { const severitiesQuery = this.getSeverityQueryParameter(severities); const statusesQuery = this.getStatusQueryParameter(showCleared); const _filter = { pageSize: 50, withTotalPages: true, ...(severitiesQuery && { severity: severitiesQuery }), ...(statusesQuery && { status: statusesQuery }), ...(selectedDates && { lastUpdatedFrom: selectedDates[0].toISOString(), createdTo: selectedDates[1].toISOString() }), ...filter }; return this.alarmService.list(_filter); } retrieveAlarmsByDate(dates) { return this.alarmService.list({ lastUpdatedFrom: dates[0].toISOString(), createdTo: dates[1].toISOString(), pageSize: 50, withTotalPages: true }); } /** * Updates the state to enable or disable intervals. * @param value - A boolean value to indicate whether to enable intervals. */ updateIntervalState(value) { this._isIntervalEnabled?.next(value); } /** * Fetches the count of alarms filtered by severity and clearance status. * * @param severity - The severity level to filter by (e.g., CRITICAL, MAJOR, etc.). * @param showCleared - Whether or not to include cleared alarms in the count. * @param filter - Additional filter criteria for alarms. * * @returns A promise that resolves to the number of alarms that match the filter criteria. * */ async getAlarmsCountBySeverity(severity, showCleared, filter) { const statusesQuery = this.getStatusQueryParameter(showCleared); const _filter = { ...(severity && { severity: severity }), ...(statusesQuery && { status: statusesQuery }), ...filter }; const { data } = await this.alarmService.count(_filter); return data; } /** * Retrieves the current alarms refresh type from the OptionsService * and determines whether it is set to "interval". * * @returns `true` if the alarms refresh type is "interval," otherwise `false`. */ isIntervalRefresh() { const value = this.optionsService.get(this.ALARM_REFRESH_TYPE_KEY, 'interval'); return value === 'interval'; } /** * Updates the list of selected severities based on the new severity filter. * * @param severityUpdates - The object representing the updates to each severity. * * @returns An array representing the updated selected severities. */ updateSelectedSeverities(severityUpdates) { return Object.keys(severityUpdates) .filter(key => severityUpdates[key]) .map(key => key.toUpperCase()); } /** * Clears all active alarms of the selected severities. * * This method clears all active alarms for the given list of severities by making bulk update calls. If no severities are selected, it defaults to using all available severities. * It works by sending a series of update requests for each severity and returns a Promise that resolves with an object indicating if all alarms were resolved immediately. * * @param selectedSeverities An array of severities to be cleared. If not provided, all severities will be cleared. * @param sourceId - Identifier for the source associated with the alarms to be cleared. * * @returns A Promise that resolves with an object with a flag `resolvedImmediately`. The flag is true if all alarms for all selected severities were cleared successfully; otherwise false. * * **Example** * ```typescript * const severitiesToClear: SeverityType[] = [Severity.MAJOR, Severity.MINOR]; * * clearAllActiveAlarms(severitiesToClear).then(({ resolvedImmediately }) => { * if (resolvedImmediately) { * console.log('All selected alarms were cleared successfully.'); * } else { * console.log('Some alarms could not be cleared.'); * } * }); * ``` * * **Note** * - The method uses the `alarmService.updateBulk` for each severity to clear the active alarms. * - It may fetch the `sourceId` based on the view (if applicable) and include it as a query parameter in the update calls. * - The method returns immediately but the returned Promise needs to have a `then` or `catch` method call to handle the result or error respectively. * - Uses `Promise.all` to wait for all update requests to complete before resolving the final result. */ async clearAllActiveAlarms(selectedSeverities, sourceId) { const severitiesToUpdate = selectedSeverities || Severity; const promises = Object.values(severitiesToUpdate).map((severity) => { const commonParams = { resolved: false, severity }; const parameters = sourceId ? { ...commonParams, source: sourceId, withSourceAssets: true, withSourceDevices: true } : commonParams; return this.alarmService.updateBulk({ status: AlarmStatus.CLEARED }, parameters); }); const responses = await Promise.all(promises); return { resolvedImmediately: responses.every(res => res) }; } /** * Returns the correct link based on the provided context data. * @param contextData The context the navigation was triggered from. * @param alarm The alarm to navigate to. * @returns A link to be used as an url navigation. */ getRouterLink(contextData, alarm) { let detailUrl = `/${ALARMS_PATH}`; if (alarm) { detailUrl = `/${ALARMS_PATH}/${alarm.id}`; } if (!contextData) { return detailUrl; } switch (contextData.context) { case ViewContext.Device: return `/device/${contextData.contextData.id}${detailUrl}`; case ViewContext.Group: return `/group/${contextData.contextData.id}${detailUrl}`; case ViewContext.Simulators: return `/simulators/${contextData.contextData.id}${detailUrl}`; default: return detailUrl; } } /** * Returns the correct array navigation. * @param contextData The context the navigation was triggered from. * @param alarm The alarm to navigate to. * @returns A link to be used as a router.navigation. */ getRouterNavigationArray(contextData, alarm) { return this.getRouterLink(contextData, alarm).split('/').filter(Boolean); } /** * Returns the correct from and to dates based on the selected interval * @param intervalId the selected interval. E.g. 'none', 'hours', 'custom' ... * @returns The calculated date context based on the selected interval. */ getDateTimeContextByInterval(intervalId) { const interval = INTERVALS_EXTENDED.find(({ id }) => id === intervalId); if (interval.id === 'none') { return [new Date(0), new Date()]; } const dateTo = new Date(); const dateFrom = new Date(dateTo.valueOf() - interval.timespanInMs); return [dateFrom, dateTo]; } /** * Creates a value for query parameter for filtering alarms by severity based on array of selected severities. * * @param severities - An array of alarm severity types to include in the filter. * If the array is empty or undefined, no severity filter will be applied. * * @returns A comma-separated string of selected alarm severities, * or null if no severities are provided. */ getSeverityQueryParameter(severities) { if (!severities || severities.length === 0) { return; } if (severities.length === Object.keys(Severity).length) { return; } return severities.join(','); } /** * Creates a value for query parameter for filtering alarms by statuses based on showCleared option. * * @param showCleared - A flag indicating whether to include cleared statuses. * If true, all statuses, including 'CLEARED', will be included; if false, 'CLEARED' will be excluded. * * @returns A comma-separated string of alarm statuses. */ getStatusQueryParameter(showCleared) { const statuses = Object.keys(ALARM_STATUS_LABELS); const filteredStatuses = showCleared ? statuses : statuses.filter(status => status !== 'CLEARED'); return filteredStatuses.join(','); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlarmsViewService, deps: [{ token: i2.AlarmService }, { token: i3.OptionsService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlarmsViewService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlarmsViewService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i2.AlarmService }, { type: i3.OptionsService }] }); /** * Pipe for transforming alarm severity types into corresponding icons. * * @example * Usage in an Angular template: * {{ 'CRITICAL' | AlarmSeverityToIcon }} * Result: 'exclamation-circle' */ class AlarmSeverityToIconPipe { /** * Transforms an alarm severity type into a corresponding icon. * * @param alarmSeverity - The severity type of the alarm. * @returns The corresponding icon for the given alarm severity type. */ transform(alarmSeverity) { const alarmSeverityMapped = Severity[alarmSeverity?.toUpperCase()]; return ALARM_SEVERITY_ICON_MAP[alarmSeverityMapped] || HELP_ICON; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlarmSeverityToIconPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "18.2.13", ngImport: i0, type: AlarmSeverityToIconPipe, isStandalone: true, name: "AlarmSeverityToIcon" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlarmSeverityToIconPipe, decorators: [{ type: Pipe, args: [{ name: 'AlarmSeverityToIcon', standalone: true }] }] }); /** * Angular pipe for transforming alarm statuses into corresponding icons. * * @example * Usage in an Angular template: * {{ 'ACTIVE' | AlarmStatusToIcon }} * Result: 'bell' */ class AlarmStatusToIconPipe { /** * Transforms an alarm status into a corresponding icon. * * @param alarmStatus - The status of the alarm. * @returns - The corresponding icon for the given alarm status. */ transform(alarmStatus) { return AlarmIconMap[alarmStatus?.toUpperCase()] || HELP_ICON; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlarmStatusToIconPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "18.2.13", ngImport: i0, type: AlarmStatusToIconPipe, name: "AlarmStatusToIcon" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlarmStatusToIconPipe, decorators: [{ type: Pipe, args: [{ name: 'AlarmStatusToIcon' }] }] }); /** * A pipe for transforming audit record data into localized messages. * It specifically addresses changes in the audit records, with an emphasis on status changes. */ class AuditChangesMessagePipe { constructor(translateService) { this.translateService = translateService; } /** * Transforms an IAuditRecord into a localized string message. * If the record contains changes, and if the first change is related to the 'status' attribute, * it formats a message indicating the status change. Otherwise, it returns a general activity message. * Example when there is a status change: "Alarm status changed from ACKNOWLEDGED to ACTIVE". * Example when record does not have a status attribute: "Alarm updated". * * @param record - The audit record to be transformed. * @returns The localized message describing the audit record, * particularly focusing on status changes if applicable. */ transform(record) { const firstItem = !!record.changes && Array.from(record.changes)[0]; if (!firstItem || firstItem.attribute !== 'status') { const activityString = gettext(record.activity); return this.translateService.instant(activityString); } const { newValue, previousValue } = firstItem; const message = gettext(`Alarm status changed from {{ previousValue }} to {{ newValue }}`); return this.translateService.instant(message, { previousValue: this.translateService.instant(previousValue), newValue: this.translateService.instant(newValue) }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AuditChangesMessagePipe, deps: [{ token: i1.TranslateService }], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "18.2.13", ngImport: i0, type: AuditChangesMessagePipe, name: "auditChangesMessage" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AuditChangesMessagePipe, decorators: [{ type: Pipe, args: [{ name: 'auditChangesMessage' }] }], ctorParameters: () => [{ type: i1.TranslateService }] }); class AlarmDetailsComponent { constructor(alarmDetailsService, alarmService, alertService, appState, auditService, relativeTime, ng1SmartRulesUpgradeService, translateService, inventoryService, alarmsViewService, colorService, interAppService) { this.alarmDetailsService = alarmDetailsService; this.alarmService = alarmService; this.alertService = alertService; this.appState = appState; this.auditService = auditService; this.relativeTime = relativeTime; this.ng1SmartRulesUpgradeService = ng1SmartRulesUpgradeService; this.translateService = translateService; this.inventoryService = inventoryService; this.alarmsViewService = alarmsViewService; this.colorService = colorService; this.interAppService = interAppService; this.ACKNOWLEDGED_STATUS_VALUE = AlarmStatus.ACKNOWLEDGED; this.ACTIVE_STATUS_VALUE = AlarmStatus.ACTIVE; this.CLEARED_STATUS_VALUE = AlarmStatus.CLEARED; this.ACKNOWLEDGE_LABEL = gettext('Acknowledge'); this.REACTIVATE_LABEL = gettext('Reactivate'); this.SEVERITY_LABELS = SEVERITY_LABELS; this.BELL_SLASH_ICON = ALARM_STATUS_ICON.BELL_SLASH; this.BELL_ICON = ALARM_STATUS_ICON.BELL; this.deviceManagementAppKey = SupportedApps.devicemanagement; this.linkTitle = gettext('Open in {{ appName }}'); this.PAGE_SIZE = 100; /** * Indicates when alarms status change was started (Acknowledge/Reactivate) */ this.isAlarmStatusChanging = false; /** * Custom fragments of the selected alarm. If none exist, null is returned. */ this.customFragments = null; } async ngOnInit() { const isSmartRulesServiceSubscribed = !!(await firstValueFrom(this.interAppService.getApp$(SupportedApps.smartrules))); const hasAnyRoleAllowingToCreateSmartRule = this.alarmDetailsService.checkIfHasAnyRoleAllowingToCreateSmartRule(); this.isCreateSmartRulesButtonAvailable = !!this.ng1SmartRulesUpgradeService && isSmartRulesServiceSubscribed && hasAnyRoleAllowingToCreateSmartRule; this.userDeviceManagementApp$ = this.interAppService.getApp$(this.deviceManagementAppKey); this.showSourceNavigationLink$ = this.interAppService.shouldShowAppLink$(this.deviceManagementAppKey); this.typeColor = await this.colorService.generateColor(this.selectedAlarm.type); } async ngOnChanges(changes) { if (changes.selectedAlarm && changes.selectedAlarm.currentValue) { await this.reloadAuditLog(true, true); await this.updateStatusMessage(); const { data } = await this.inventoryService.detail(this.selectedAlarm.source.id); this.selectedAlarmMO = data; this.customFragments = this.getCustomFragments(this.selectedAlarm); } } createSmartRule() { if (!this.isCreateSmartRulesButtonAvailable) { return; } this.ng1SmartRulesUpgradeService.addNewForInputAlarmAndOutputUserWithUI(this.selectedAlarm, this.appState.currentUser.value); } /** * Navigates to a specific alarm source device based on the provided source. * * @param sourceId - The source id. */ async goToAlarmSource(sourceId) { const { data } = await this.alarmService.detail(sourceId); await this.interAppService.navigateToApp(this.deviceManagementAppKey, `#/device/${data.source.id}/alarms`); } /** * Reloads audit log data asynchronously. * * This method fetches audit records using `getAlarmAuditRecords` and optionally updates the audit logs * state in the component based on the `isSetAuditLogs` flag. It handles the loading state and potential * errors during the fetch operation. * * @param isRevert - A boolean flag indicating whether to retrieve a 100 (see PAGE_SIZE) records (true) * or only record, that chronologically will be the oldest one (false). Defaults to true. * If set to false, it will set PAGE_SIZE to 1 and trigger a logic * concatenating a most recent record with the very first one to * calculate the alarm duration (change to CLEARED status). * It's passed to the `getAlarmAuditRecords` method. * @param isSetAuditLogs - A boolean flag to determine if the fetched audit logs should be set in the component state. Defaults to `false`. * @returns A promise that resolves to a list of `IAuditRecord` objects. */ async reloadAuditLog(isRevert = true, isSetAuditLogs = false) { try { this.isLoading = true; const auditLogs = await this.getAlarmAuditRecords(isRevert); if (isSetAuditLogs) { this.setAuditLogs(auditLogs); } return auditLogs; } catch (error) { this.alertService.addServerFailure(error); } finally { this.isLoading = false; } } async onUpdateDetails(status) { try { this.isAlarmStatusChanging = true; await this.updateAlarmStatus(status); await this.reloadAuditLog(true, true); await this.updateStatusMessage(); this.updateLastUpdatedDate(this.auditLog.data[0]); } catch (error) { this.alertService.addServerFailure(error); } finally { this.isAlarmStatusChanging = false; } } async detailsButtonAction(button, alarm) { const result = button.action(alarm); let shouldReload = false; if (result instanceof Promise) { shouldReload = await result; } else { shouldReload = result; } if (shouldReload) { let alarm; if (shouldReload === true) { const { data: updatedAlarm } = await this.alarmService.detail(this.selectedAlarm.id); alarm = updatedAlarm; } else { alarm = shouldReload; } this.alarmsViewService.updateAlarmList(); const previousValue = this.selectedAlarm; this.selectedAlarm = alarm; this.ngOnChanges({ selectedAlarm: { currentValue: alarm, previousValue, firstChange: false, isFirstChange: () => false } }); } } async updateAlarmStatus(status) { const partiallyUpdatedAlarm = { id: this.selectedAlarm.id, status }; await this.alarmService.update(partiallyUpdatedAlarm); const translatedStatusLabel = this.translateService.instant(ALARM_STATUS_LABELS[status]); this.alertService.success(this.translateService.instant(gettext('Alarm status changed to {{ status }}'), { status: translatedStatusLabel.toUpperCase() })); this.selectedAlarm.status = status; this.alarmsViewService.updateAlarmList(); } /** * Retrieves the audit log and appends the last audit record to it. * * This method fetches the existing audit log data and makes a deep copy of it. It then * retrieves the last audit record and appends it to the copied audit log data. This is * useful for scenarios where the most recent audit record needs to be included in the * existing audit log data (calculating the CLEARED period). * * @returns A promise of `IResultList<IAuditRecord>`, which includes the * existing audit log data along with the last audit record appended. * @private */ async auditLogWithFirstRecord() { const existingData = this.auditLog; const copiedExistingData = cloneDeep(existingData); const lastAuditRecord = await this.reloadAuditLog(false); const lastRecord = lastAuditRecord.data[lastAuditRecord.data.length - 1]; copiedExistingData.data.push(lastRecord); return copiedExistingData; } setAuditLogs(auditLogs) { this.auditLog = auditLogs; } updateLastUpdatedDate(updatedAuditRecords) { if (!updatedAuditRecords) { return; } const { creationTime } = updatedAuditRecords; this.selectedAlarm.lastUpdated = creationTime; } getActiveStatusMessage(time) { return this.translateService.instant(gettext('ACTIVE`alarm`: triggered {{alarmTimeFromNow}}'), { alarmTimeFromNow: this.relativeTime.transform(new Date(time)) }); } getAcknowledgedStatusMessage(status, changeLog) { if (changeLog.length === 0) { return this.translateService.instant(gettext('ACKNOWLEDGED`alarm`')); } const acknowledgedBy = this.alarmDetailsService.getAcknowledgedBy(status, changeLog); const acknowledgeTime = this.alarmDetailsService.getAcknowledgeTime(changeLog); if (acknowledgedBy) { return this.translateService.instant(gettext('ACKNOWLEDGED`alarm` by: {{ackBy}} {{ackTimeFromNow}}'), { ackBy: acknowledgedBy, ackTimeFromNow: this.relativeTime.transform(new Date(acknowledgeTime)) }); } return this.translateService.instant(gettext('ACKNOWLEDGED`alarm` {{ackTimeFromNow}}'), { ackTimeFromNow: this.relativeTime.transform(new Date(acknowledgeTime)) }); } getClearedStatusMessage(auditLog) { if (auditLog.length === 0) { return this.translateService.instant(gettext('CLEARED`alarm`')); } const differenceInMs = this.calculateAlarmDuration(auditLog); return this.translateService.instant(gettext('CLEARED`alarm`: was active for {{alarmDuration}}'), { alarmDuration: this.relativeTime.transform(differenceInMs, true) }); } /** * Calculates the duration of an alarm based on audit log records. * * This method computes the duration of an alarm by finding the difference * between the start and end times of the alarm. The start time is determined * from the last record in the audit log, using the first available time field * (`firstOccurrenceTime`, `time`, or `creationTime`). The end time is obtained * from the `alarmDetailsService`. * * @param auditLog - An array of `IAuditRecord` objects representing the audit log records. * @returns The duration of the alarm in milliseconds, or `null` if the end time is not available. * @private */ calculateAlarmDuration(auditLog) { const firstAlarm = auditLog[auditLog.length - 1]; const startTime = firstAlarm.firstOccurrenceTime || firstAlarm.time || firstAlarm.creationTime; const endTime = this.alarmDetailsService.getEndTime(auditLog); if (!endTime) { return null; } const startTimeToDate = new Date(startTime); const endTimeToDate = new Date(endTime); return endTimeToDate.getTime() - startTimeToDate.getTime(); } /** * Retrieves a list of audit records for a selected alarm. * * This method fetches audit records based on the specified properties, including * the date, page size, whether to revert, the source alarm ID, and whether to include total pages. * * @param isRevert - A boolean flag indicating whether to retrieve a 100 (see PAGE_SIZE) records (true) * or only record, that chronologically will be the oldest one (false). Defaults to true. * If set to false, it will set PAGE_SIZE to 1 and trigger a logic * concatenating a most recent record with the very first one to * calculate the alarm duration (change to CLEARED status). * @returns A Promise that resolves to an IResultList of IAuditRecord objects, representing the audit records. * @async * @private */ async getAlarmAuditRecords(isRevert = true) { const properties = { dateTo: new Date(Date.now()).toISOString(), pageSize: isRevert ? this.PAGE_SIZE : 1, revert: isRevert, source: this.selectedAlarm.id, withTotalPages: true }; return await this.auditService.list(properties); } async updateStatusMessage() { switch (this.selectedAlarm.status) { case this.ACTIVE_STATUS_VALUE: this.statusMessage = this.getActiveStatusMessage(this.selectedAlarm.time); break; case this.ACKNOWLEDGED_STATUS_VALUE: this.statusMessage = this.getAcknowledgedStatusMessage(this.selectedAlarm.status, this.auditLog.data); break; case this.CLEARED_STATUS_VALUE: if (this.hasReachedOrExceededPageSizeLimit()) { this.extendedAuditLogs = await this.auditLogWithFirstRecord(); this.statusMessage = this.getClearedStatusMessage(this.extendedAuditLogs.data); return; } this.statusMessage = this.getClearedStatusMessage(this.auditLog.data); break; } } hasReachedOrExceededPageSizeLimit() { return this.auditLog.data.length >= this.PAGE_SIZE; } getCustomFragments(selectedAlarm) { let customProperties = null; for (const key in selectedAlarm) { if (!ALARM_DEFAULT_PROPERTIES.find(k => k === key)) { if (!customProperties) { customProperties = {}; } customProperties[key] = selectedAlarm[key]; } } return customProperties; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AlarmDetailsComponent, deps: [{ token: AlarmDetailsService }, { token: i2.AlarmService }, { token: i3.AlertService }, { token: i3.AppStateService }, { token: i2.AuditService }, { token: i3.RelativeTimePipe }, { token: Ng1SmartRulesUpgradeService, optional: true }, { token: i1.TranslateService }, { token: i2.InventoryService }, { token: AlarmsViewService }, { token: i3.ColorService }, { token: i3.InterAppService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: AlarmDetailsComponent, selector: "c8y-alarm-details", inputs: { selectedAlarm: "selectedAlarm" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"d-flex row tight-grid flex-wrap a-i-stretch\">\n <div class=\"col-xs-12 col-md-6 d-flex p-b-8\">\n <div\n class=\"border-all fit-w d-flex\"\n data-cy=\"c8y-alarm-details--status-section-wrapper\"\n >\n <div\n class=\"p-8\"\n data-cy=\"c8y-alarm-details--status-icon\"\n >\n <i\n class=\"icon-24 text-gray-dark m-t-4 c8y-icon\"\n [c8yIcon]=\"selectedAlarm.status | AlarmStatusToIcon\"\n ></i>\n </div>\n <div class=\"p-t-8 p-b-8 p-r-8\">\n <p class=\"text-label-small m-b-0 m-r-8\">{{ 'Status' | translate }}</p>\n <p class=\"small\">{{ statusMessage }}</p>\n </div>\n </div>\n </div>\n <div class=\"col-xs-12 col-md-6 d-flex p-b-8\">\n <div\n class=\"border-all fit-w d-flex\"\n data-cy=\"c8y-alarm-details--severity-section-wrapper\"\n >\n <div class=\"p-8\">\n <i\n class=\"icon-24 text-gray-dark m-t-4 stroked-icon status\"\n [c8yIcon]=\"selectedAlarm.severity | AlarmSeverityToIcon\"\n [ngClass]=\"selectedAlarm.severity?.toString() | lowercase\"\n ></i>\n </div>\n <div class=\"p-t-8 p-b-8 p-r-8\">\n <p class=\"text-label-small m-b-0 m-r-8\">{{ 'Severity' | translate }}</p>\n <p class=\"small\">{{ SEVERITY_LABELS[selectedAlarm.severity] | translate }}</p>\n </div>\n </div>\n </div>\n <div\n class=\"col-xs-12 col-md-6 d-flex p-b-8\"\n data-cy=\"c8y-alarm-details--source-wrapper\"\n >\n <div class=\"border-all fit-w d-flex\">\n <div class=\"p-8\">\n <i\n class=\"icon-24 text-gray-dark m-t-4 stroked-icon status\"\n c8yIcon=\"contactless-payment\"\n ></i>\n </div>\n <div class=\"p-t-8 p-b-8 p-r-8\">\n <p class=\"text-label-small m-b-0 m-r-8\">{{ 'Source' | translate }}</p>\n <p class=\"small\">\n <button\n class=\"btn-link text-muted p-0 m-r-8 text-left\"\n title=\"{{ selectedAlarm.source.name }}\"\n type=\"button\"\n routerLink=\"{{ selectedAlarmMO | assetLink }}\"\n >\n <small class=\"icon-flex\">\n <i c8yIcon=\"exchange\"></i>\n {{ selectedAlarm.source.name || selectedAlarm.source.id }}\n </small>\n </button>\n <ng-container *ngIf=\"showSourceNavigationLink$ | async\">\n <button\n class=\"btn-link p-0 text-left\"\n title=\"{{\n linkTitle\n | translate\n : { appName: userDeviceManagementApp$ | async | humanizeAppName | async }\n }}\"\n type=\"button\"\n (click)=\"goToAlarmSource(selectedAlarm.id)\"\n data-cy=\"alarm-details-device-management-link\"\n >\n {{ userDeviceManagementApp$ | async | humanizeAppName | async }}\n <i c8yIcon=\"external-link\"></i>\n </button>\n </ng-container>\n </p>\n </div>\n </div>\n </div>\n <div\n class=\"col-xs-12 col-md-6 d-flex p-b-8\"\n data-cy=\"c8y-alarm-details--severity-type-wrapper\"\n >\n <div class=\"border-all fit-w d-flex\">\n <div class=\"p-8\">\n <span\n class=\"circle-icon-wrapper\"\n [ngStyle]=\"{ 'background-color': typeColor }\"\n >\n <i\n class=\"stroked-icon\"\n c8yIcon=\"bell\"\n ></i>\n </span>\n </div>\n <div class=\"p-t-8 p-b-8 p-r-8 min-width-0\">\n <p class=\"text-label-small m-b-0 m-r-8\">{{ 'Type' | translate }}</p>\n <p\n class=\"small text-truncate\"\n title=\"{{ selectedAlarm.type }}\"\n >\n <code>{{ selectedAlarm.type }}</code>\n </p>\n </div>\n </div>\n </div>\n\n <div class=\"col-xs-12 col-md-12 p-b-16\">\n <div class=\"border-all fit-w d-flex\">\n <div class=\"p-8\">\n <i\n class=\"icon-24 text-gray-dark m-t-4\"\n data-cy=\"c8y-alarm-details--last-updated-icon\"\n c8yIcon=\"calendar\"\n data-cy=\"c8y-alarm-details--last-updated-icon\"\n ></i>\n </div>\n <div class=\"p-t-8 p-b-0 p-r-8 flex-grow\">\n <div class=\"content-flex-50\">\n <div\n class=\"col-4 p-b-8\"\n *ngIf=\"selectedAlarm.count > 1\"\n data-cy=\"c8y-alarm-details--number-of-occurrences-wrapper\"\n >\n