UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1,043 lines (1,034 loc) 243 kB
import * as i0 from '@angular/core'; import { Injectable, Pipe, InjectionToken, HostListener, Input, Optional, Component, EventEmitter, ViewChild, Output, ChangeDetectionStrategy, forwardRef, signal, NgModule } from '@angular/core'; import { combineLatest, BehaviorSubject, Subject, fromEvent, firstValueFrom, of, from, forkJoin, EMPTY, Observable, pipe, take, takeUntil as takeUntil$1, map as map$1 } from 'rxjs'; import { filter, map, switchMap, startWith, debounceTime, takeUntil, distinctUntilChanged, catchError, finalize, tap, shareReplay, throttleTime } from 'rxjs/operators'; import * as i3 from '@c8y/ngx-components'; import { Permissions, ViewContext, SupportedApps, IconDirective, EmptyStateComponent, LoadingComponent, ListGroupComponent, ForOfDirective, ListItemTimelineComponent, ListItemComponent, ListItemBodyComponent, SplitViewDetailsActionsComponent, IconPanelComponent, C8yTranslatePipe, DatePipe, HumanizeAppNamePipe, AssetLinkPipe, MarkdownToHtmlPipe, TitleComponent, TabsOutletComponent, ProductExperienceDirective, RequiredInputPlaceholderDirective, CountdownIntervalComponent, DynamicComponentAlertAggregator, DynamicComponentAlert, C8yTranslateDirective, ListItemIconComponent, DynamicComponentAlertsComponent, SplitViewListComponent, SplitViewHeaderActionsComponent, SplitViewAlertsComponent, FormGroupComponent, DateTimePickerComponent, MessagesComponent, MessageDirective, ListItemCheckboxComponent, SplitViewComponent, SplitViewDetailsComponent, HelpComponent, ActionBarItemComponent, AlarmWithChildrenRealtimeService, RouterTabsResolver, ContextRouteGuard, ContextRouteComponent, hookNavigator, hookRoute, CommonModule, CoreModule, HeaderModule, C8yTranslateModule, DynamicComponentModule, RelativeTimePipe } from '@c8y/ngx-components'; import { sortBy, cloneDeep } from 'lodash-es'; import { NgClass, NgStyle, NgTemplateOutlet, AsyncPipe, JsonPipe, LowerCasePipe, NgIf, NgFor, TitleCasePipe } from '@angular/common'; import * as i1 from '@angular/router'; import { RouterLink, NavigationEnd, RouterLinkActive, RouterOutlet, RouterModule } from '@angular/router'; import * as i2 from '@c8y/client'; import { AlarmStatus, Severity, ALARM_STATUS_LABELS, SEVERITY_LABELS } from '@c8y/client'; import { gettext } from '@c8y/ngx-components/gettext'; import * as i1$1 from '@ngx-translate/core'; import { PopoverDirective, PopoverModule } from 'ngx-bootstrap/popover'; import * as i3$1 from '@c8y/ngx-components/global-context'; import { INTERVAL_TITLES, INTERVALS, IntervalPickerComponent } from '@c8y/ngx-components/interval-picker'; import * as i1$2 from '@angular/forms'; import { FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; import { BsDropdownDirective, BsDropdownToggleDirective, BsDropdownMenuDirective, BsDropdownModule } from 'ngx-bootstrap/dropdown'; import { CdkTrapFocus, A11yModule } from '@angular/cdk/a11y'; import * as i1$4 from 'ngx-bootstrap/tooltip'; import { TooltipDirective, 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: "20.3.19", 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: "20.3.19", ngImport: i0, type: AlarmDetailsButtonService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", 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: "20.3.19", ngImport: i0, type: AlarmDetailsButtonPipe, deps: [{ token: AlarmDetailsButtonService }], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.19", ngImport: i0, type: AlarmDetailsButtonPipe, isStandalone: true, name: "alarmDetailsButton" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", 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: "20.3.19", ngImport: i0, type: AlarmDetailsService, deps: [{ token: i3.Permissions }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AlarmDetailsService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AlarmDetailsService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i3.Permissions }] }); class AlarmsActivityTrackerService { constructor() { this.isUserActive$ = new BehaviorSubject(true); this.userSecondsSpendOnPage = 0; this.INACTIVITY_THRESHOLD_SECONDS = 10; this.ONE_SECOND_IN_MILLISECONDS = 1_000; this.destroy$ = new Subject(); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } setGainsightInterval() { this.gainsightTimerId = setInterval(() => this.userSecondsSpendOnPage++, this.ONE_SECOND_IN_MILLISECONDS); } clearGainsightInterval() { clearInterval(this.gainsightTimerId); } resetInactivityTimer() { this.isUserActive$.next(true); clearTimeout(this.gainsightInactivityTimeoutId); this.gainsightInactivityTimeoutId = setTimeout(() => { this.isUserActive$.next(false); // Pause counting if the user is inactive }, this.INACTIVITY_THRESHOLD_SECONDS * this.ONE_SECOND_IN_MILLISECONDS); } setupEventListenersForGainsight() { const events = ['mousemove', 'keydown', 'click']; events.forEach(event => { fromEvent(window, event) .pipe(debounceTime(30), takeUntil(this.destroy$)) .subscribe(() => this.resetInactivityTimer()); }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AlarmsActivityTrackerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AlarmsActivityTrackerService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AlarmsActivityTrackerService, decorators: [{ type: Injectable }] }); 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; const PRODUCT_EXPERIENCE_ALARMS = { EVENTS: { ALARMS: 'Alarms' }, COMPONENTS: { ALARMS_FILTER: 'alarms-filter', ALARMS_INTERVAL_REFRESH: 'alarms-interval-refresh', ALARMS: 'alarms', ALARMS_TYPE_FILTER: 'alarms-type-filter', ALARM_DETAILS: 'alarm-details' }, ACTIONS: { APPLY_FILTER: 'applyFilter', REMOVE_CHIP_FILTER: 'removeChipFilter', APPLY_TYPE_FILTER: 'applyTypeFilter', CREATE_SMART_RULE: 'createSmartRule', ACKNOWLEDGE_ALARM: 'acknowledgeAlarm', REACTIVATE_ALARM: 'reactivateAlarm', CLEAR_ALARM: 'clearAlarm', RELOAD_AUDIT_LOGS: 'reloadAuditLogs', USER_SPEND_TIME_ON_COMPONENT: 'userSpendTimeOnComponent' } }; /** * 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, dateTimeContextPickerService, router, contextRouteService) { this.alarmService = alarmService; this.optionsService = optionsService; this.dateTimeContextPickerService = dateTimeContextPickerService; this.router = router; this.contextRouteService = contextRouteService; 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(); this.closeDetailsView$ = new Subject(); if (this.isIntervalRefresh()) { this._isIntervalEnabled = new Subject(); this.isIntervalEnabled$ = this._isIntervalEnabled.asObservable(); } } /** * Emits a subject to initialize the alarms reloading. */ updateAlarmList(value = null) { this.reloadAlarmsList$.next(value); } /** * 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); } /** * Closes the details view and navigates based on the current route context, * preserving existing query parameters. */ async closeDetailsView(activatedRoute) { const contextData = this.contextRouteService.getContextData(activatedRoute); await this.router.navigate(this.getRouterNavigationArray(contextData), { queryParamsHandling: 'merge' }); this.updateIntervalState(true); } /** * 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) { return this.dateTimeContextPickerService.getDateTimeContextByInterval(intervalId); } /** * Converts a given number of seconds into a formatted string representing hours, minutes, and seconds. * * @param totalSeconds - The total number of seconds to convert. * @returns A string in the format "HH:MM:SS", where HH is hours, MM is minutes, and SS is seconds. */ convertSecondsToTime(totalSeconds) { const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = Math.floor(totalSeconds % 60); const paddedHours = hours.toString().padStart(2, '0'); const paddedMinutes = minutes.toString().padStart(2, '0'); const paddedSeconds = seconds.toString().padStart(2, '0'); return `${paddedHours}:${paddedMinutes}:${paddedSeconds}`; } /** * 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: "20.3.19", ngImport: i0, type: AlarmsViewService, deps: [{ token: i2.AlarmService }, { token: i3.OptionsService }, { token: i3$1.DateTimeContextPickerService }, { token: i1.Router }, { token: i3.ContextRouteService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AlarmsViewService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AlarmsViewService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i2.AlarmService }, { type: i3.OptionsService }, { type: i3$1.DateTimeContextPickerService }, { type: i1.Router }, { type: i3.ContextRouteService }] }); /** * 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: "20.3.19", ngImport: i0, type: AuditChangesMessagePipe, deps: [{ token: i1$1.TranslateService }], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.19", ngImport: i0, type: AuditChangesMessagePipe, isStandalone: true, name: "auditChangesMessage" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AuditChangesMessagePipe, decorators: [{ type: Pipe, args: [{ name: 'auditChangesMessage' }] }], ctorParameters: () => [{ type: i1$1.TranslateService }] }); /** * 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'] }; class AlarmDetailsComponent { constructor(alarmDetailsService, alarmService, alertService, appState, auditService, relativeTime, ng1SmartRulesUpgradeService, translateService, inventoryService, alarmsViewService, colorService, interAppService, gainsightService, alarmsActivityTrackerService) { 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.gainsightService = gainsightService; this.alarmsActivityTrackerService = alarmsActivityTrackerService; /** * Master switch to show/hide ALL alarm details sections (default: true). * When `false`, all sections are hidden regardless of `hiddenSections`. * When `true`, use `hiddenSections` for granular control of individual sections. * * Priority: `showSections` takes precedence over `hiddenSections`. */ this.showSections = true; /** * Master switch to show/hide ALL action buttons (default: true). * When `false`, all actions are hidden regardless of `hiddenActions`. * When `true`, use `hiddenActions` for granular control of individual actions. * * Priority: `showActions` takes precedence over `hiddenActions`. */ this.showActions = true; /** * Whether to show external navigation links (default: true) */ this.showExternalNavigation = true; 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.PRODUCT_EXPERIENCE_ALARMS = PRODUCT_EXPERIENCE_ALARMS; 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; /** * Cached alarm actions to prevent constant re-rendering */ this.alarmActions = []; /** * Cached alarm info sections to prevent constant re-rendering */ this.alarmInfoSections = []; /** * Custom fragments of the selected alarm. If none exist, null is returned. */ this.customFragments = null; this.USER_MINIMUM_SPEND_TIME_SECONDS_TO_TRIGGER_EVENT = 1; this.destroy$ = new Subject(); } async ngOnInit() { this.alarmsActivityTrackerService.setupEventListenersForGainsight(); this.alarmsActivityTrackerService.resetInactivityTimer(); this.alarmsActivityTrackerService.isUserActive$ .pipe(distinctUntilChanged(), takeUntil(this.destroy$)) .subscribe(isActive => isActive ? this.alarmsActivityTrackerService.setGainsightInterval() : this.alarmsActivityTrackerService.clearGainsightInterval()); 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); // Initialize cached actions and info sections if (this.selectedAlarm) { this.updateAlarmActions(); this.updateAlarmInfoSections(); } } 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); // Update cached actions and info sections when alarm data changes this.updateAlarmActions(); this.updateAlarmInfoSections(); } } ngOnDestroy() { if (this.alarmsActivityTrackerService.userSecondsSpendOnPage >= this.USER_MINIMUM_SPEND_TIME_SECONDS_TO_TRIGGER_EVENT) { this.gainsightService.triggerEvent(PRODUCT_EXPERIENCE_ALARMS.EVENTS.ALARMS, { component: PRODUCT_EXPERIENCE_ALARMS.COMPONENTS.ALARM_DETAILS, action: PRODUCT_EXPERIENCE_ALARMS.ACTIONS.USER_SPEND_TIME_ON_COMPONENT, userSpendTime: this.alarmsViewService.convertSecondsToTime(this.alarmsActivityTrackerService.userSecondsSpendOnPage) }); } this.alarmsActivityTrackerService.clearGainsightInterval(); this.destroy$.next(); this.destroy$.complete(); } visibilityChange() { if (document.hidden) { this.alarmsActivityTrackerService.clearGainsightInterval(); return; } this.alarmsActivityTrackerService.setGainsightInterval(); } 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; // Update actions to show spinner on reload button this.updateAlarmActions(); const auditLogs = await this.getAlarmAuditRecords(isRevert); if (isSetAuditLogs) { this.setAuditLogs(auditLogs); } return auditLogs; } catch (error) { this.alertService.addServerFailure(error); } finally { this.isLoading = false; // Update actions to remove spinner from reload button this.updateAlarmActions(); } } async onUpdateDetails(status) { try { this.isAlarmStatusChanging = true; // Update actions when status changing flag is set (affects button disabled state) this.updateAlarmActions(); await this.updateAlarmStatus(status); await this.reloadAuditLog(true, true); await this.updateStatusMessage(); this.updateLastUpdatedDate(this.auditLog.data[0]); // Update info sections to reflect new status message this.updateAlarmInfoSections(); if (status === AlarmStatus.CLEARED) { this.alarmsViewService.closeDetailsView$.next(); } } catch (error) { this.alertService.addServerFailure(error); } finally { this.isAlarmStatusChanging = false; // Update actions when status changing flag is cleared (re-enables button) this.updateAlarmActions(); } } 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() })); if (this.selectedAlarm) { this.selectedAlarm.status = status; } this.alarmsViewService.updateAlarmList(); // Update cached actions and info sections when status changes this.updateAlarmActions(); this.updateAlarmInfoSections(); } /** * 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; if (this.selectedAlarm) { 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); } a