@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1,043 lines (1,034 loc) • 243 kB
JavaScript
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