Browse Source

Merge pull request #8097 from rusikv/alarm-assign-ui-dev

[WIP][3.5] UI: Alarm assignment
pull/8102/head
Andrew Shvayka 4 years ago
committed by GitHub
parent
commit
33ecbb238b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      ui-ngx/src/app/core/http/alarm.service.ts
  2. 9
      ui-ngx/src/app/core/services/utils.service.ts
  3. 51
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html
  4. 101
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.scss
  5. 227
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts
  6. 99
      ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html
  7. 61
      ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.scss
  8. 53
      ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts
  9. 145
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  10. 27
      ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss
  11. 16
      ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts
  12. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  13. 24
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html
  14. 36
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss
  15. 92
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  16. 13
      ui-ngx/src/assets/locale/locale.constant-en_US.json

8
ui-ngx/src/app/core/http/alarm.service.ts

@ -60,6 +60,14 @@ export class AlarmService {
return this.http.post<void>(`/api/alarm/${alarmId}/clear`, null, defaultHttpOptionsFromConfig(config)); return this.http.post<void>(`/api/alarm/${alarmId}/clear`, null, defaultHttpOptionsFromConfig(config));
} }
public assignAlarm(alarmId: string, assigneeId: string, config?: RequestConfig): Observable<void> {
return this.http.post<void>(`/api/alarm/${alarmId}/assign/${assigneeId}`, null, defaultHttpOptionsFromConfig(config));
}
public unassignAlarm(alarmId: string, config?: RequestConfig): Observable<void> {
return this.http.delete<void>(`/api/alarm/${alarmId}/assign`, defaultHttpOptionsFromConfig(config));
}
public deleteAlarm(alarmId: string, config?: RequestConfig): Observable<void> { public deleteAlarm(alarmId: string, config?: RequestConfig): Observable<void> {
return this.http.delete<void>(`/api/alarm/${alarmId}`, defaultHttpOptionsFromConfig(config)); return this.http.delete<void>(`/api/alarm/${alarmId}`, defaultHttpOptionsFromConfig(config));
} }

9
ui-ngx/src/app/core/services/utils.service.ts

@ -25,7 +25,7 @@ import {
createLabelFromDatasource, createLabelFromDatasource,
deepClone, deepClone,
deleteNullProperties, deleteNullProperties,
guid, guid, hashCode,
isDefined, isDefined,
isDefinedAndNotNull, isDefinedAndNotNull,
isString, isString,
@ -405,6 +405,13 @@ export class UtilsService {
}); });
} }
public stringToHslColor(str: string, saturationPercentage: number, lightnessPercentage: number): string {
if (str && str.length) {
let hue = hashCode(str) % 360;
return `hsl(${hue}, ${saturationPercentage}%, ${lightnessPercentage}%)`;
}
}
public currentPerfTime(): number { public currentPerfTime(): number {
return this.window.performance && this.window.performance.now ? return this.window.performance && this.window.performance.now ?
this.window.performance.now() : Date.now(); this.window.performance.now() : Date.now();

51
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html

@ -0,0 +1,51 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field appearance="outline" [formGroup]="selectUserFormGroup" class="mat-block container">
<input matInput type="text" placeholder="{{ 'user.search' | translate }}"
#userInput
formControlName="user"
(focusin)="onFocus()"
[matAutocomplete]="userAutocomplete">
<mat-icon matSuffix>search</mat-icon>
<mat-autocomplete class="tb-assignee-autocomplete"
#userAutocomplete="matAutocomplete"
[displayWith]="displayUserFn"
(optionSelected)="selected($event)"
panelWidth="260px">
<mat-option [ngClass]="{'assigned': !assigneeId}" [value]="null">
<mat-icon class="unassigned-icon">account_circle</mat-icon>
<span style="margin-right: auto" translate>alarm.unassigned</span>
</mat-option>
<mat-option [ngClass]="{ 'assigned': assigneeId === user.id.id }"
*ngFor="let user of filteredUsers | async" [value]="user">
<span class="user-avatar" [innerHTML]="getUserInitials(user)"
[style.background-color]="getAvatarBgColor(user)">
</span>
<div fxLayout="column" fxLayoutGap="2px">
<span class="user-email" [innerHTML]="user.email | highlight:searchText"></span>
<span class="user-name" *ngIf="user.firstName || user.lastName" [innerHTML]="getFullName(user)"></span>
</div>
</mat-option>
<mat-option *ngIf="!(filteredUsers | async)?.length" [value]="null">
<span style="white-space: normal">
{{ translate.get('user.no-users-matching', {entity: searchText}) | async }}
</span>
</mat-option>
</mat-autocomplete>
</mat-form-field>

101
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.scss

@ -0,0 +1,101 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
overflow: auto;
background: #fff;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3), 0px 2px 6px 2px rgba(0, 0, 0, 0.15);
border-radius: 4px;
}
::ng-deep {
mat-form-field.container {
padding-top: 8px;
height: 340px;
font-size: 14px;
background-color: #fff;
.mat-form-field-wrapper {
margin-right: 8px;
margin-left: 8px;
padding: 0;
}
}
.mat-form-field-appearance-outline .mat-form-field-outline {
color: rgba(0, 0, 0, 0.12) !important;
}
.mat-form-field-appearance-outline.mat-focused .mat-form-field-outline-thick {
color: rgba(0, 0, 0, 0.12) !important;
}
.tb-assignee-autocomplete {
&.tb-assignee-autocomplete.mat-autocomplete-visible {
position: relative;
left: -8px;
margin-top: 8px;
box-shadow: none !important;
}
.assigned {
background-color: rgba(0, 0, 0, 0.06);
}
.mat-option {
font-size: 14px;
border: none;
height: 52px !important;
.unassigned-icon {
color: rgba(0, 0, 0, 0.38);
font-size: 28px;
width: 28px;
height: 28px;
margin-right: 8px;
}
.user-avatar {
display: inline-flex;
justify-content: center;
align-items: center;
align-self: center;
margin-right: 8px;
border-radius: 50%;
background-color: #5cb445;
width: 28px;
height: 28px;
color: white;
font-size: 13px;
font-weight: 700
}
.user-email {
color: rgba(0, 0, 0, 0.76);
overflow: hidden;
text-overflow: ellipsis;
max-width: 185px
}
.user-name {
color: rgba(0, 0, 0, 0.38);
font-size: 13px;
}
.mat-option-text {
display: flex;
justify-content: start;
align-items: center;
line-height: normal;
}
}
}
}

227
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts

@ -0,0 +1,227 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
Component,
ElementRef,
Inject,
InjectionToken, OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Observable, of, Subject } from 'rxjs';
import {
catchError,
debounceTime,
distinctUntilChanged,
map,
share,
switchMap,
takeUntil,
} from 'rxjs/operators';
import { User } from '@shared/models/user.model';
import { TranslateService } from '@ngx-translate/core';
import { UserService } from '@core/http/user.service';
import { PageLink } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order';
import { emptyPageData } from '@shared/models/page/page-data';
import { AlarmService } from '@core/http/alarm.service';
import { OverlayRef } from '@angular/cdk/overlay';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { UtilsService } from '@core/services/utils.service';
export const ALARM_ASSIGNEE_PANEL_DATA = new InjectionToken<any>('AlarmAssigneePanelData');
export interface AlarmAssigneePanelData {
alarmId: string;
assigneeId: string;
}
@Component({
selector: 'tb-alarm-assignee-panel',
templateUrl: './alarm-assignee-panel.component.html',
styleUrls: ['./alarm-assignee-panel.component.scss']
})
export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDestroy {
private dirty = false;
alarmId: string;
assigneeId?: string;
selectUserFormGroup: FormGroup;
@ViewChild('userInput', {static: true}) userInput: ElementRef;
filteredUsers: Observable<Array<User>>;
searchText = '';
private destroy$ = new Subject<void>();
constructor(@Inject(ALARM_ASSIGNEE_PANEL_DATA) public data: AlarmAssigneePanelData,
public overlayRef: OverlayRef,
public translate: TranslateService,
private userService: UserService,
private alarmService: AlarmService,
private fb: FormBuilder,
private utilsService: UtilsService) {
this.alarmId = data.alarmId;
this.assigneeId = data.assigneeId;
this.selectUserFormGroup = this.fb.group({
user: [null]
});
}
ngOnInit() {
this.filteredUsers = this.selectUserFormGroup.get('user').valueChanges
.pipe(
debounceTime(150),
map(value => {
return value ? (typeof value === 'string' ? value : '') : ''
}),
distinctUntilChanged(),
switchMap(name => this.fetchUsers(name)),
share(),
takeUntil(this.destroy$)
);
}
ngAfterViewInit() {
setTimeout(() => {
this.userInput.nativeElement.focus();
}, 0)
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
displayUserFn(user?: User): string | undefined {
return user ? user.email : undefined;
}
selected(event: MatAutocompleteSelectedEvent): void {
this.selectUserFormGroup.get('user').patchValue('');
const user: User = event.option.value;
if (user) {
this.assign(user);
} else {
this.unassign();
}
}
assign(user: User): void {
this.alarmService.assignAlarm(this.alarmId, user.id.id).subscribe(
() => this.overlayRef.dispose());
}
unassign(): void {
this.alarmService.unassignAlarm(this.alarmId).subscribe(
() => this.overlayRef.dispose());
}
fetchUsers(searchText?: string): Observable<Array<User>> {
this.searchText = searchText;
const pageLink = new PageLink(50, 0, searchText, {
property: 'email',
direction: Direction.ASC
});
return this.userService.getUsers(pageLink, {ignoreLoading: true})
.pipe(
catchError(() => of(emptyPageData<User>())),
map(pageData => {
return pageData.data;
})
);
}
onFocus(): void {
if (!this.dirty) {
this.selectUserFormGroup.get('user').updateValueAndValidity({onlySelf: true});
this.dirty = true;
}
}
clear() {
this.selectUserFormGroup.get('user').patchValue('', {emitEvent: true});
setTimeout(() => {
this.userInput.nativeElement.blur();
this.userInput.nativeElement.focus();
}, 0);
}
getUserDisplayName(entity: User) {
let displayName = '';
if ((entity.firstName && entity.firstName.length > 0) ||
(entity.lastName && entity.lastName.length > 0)) {
if (entity.firstName) {
displayName += entity.firstName;
}
if (entity.lastName) {
if (displayName.length > 0) {
displayName += ' ';
}
displayName += entity.lastName;
}
} else {
displayName = entity.email;
}
return displayName;
}
getUserInitials(entity: User): string {
let initials = '';
if (entity.firstName && entity.firstName.length ||
entity.lastName && entity.lastName.length) {
if (entity.firstName) {
initials += entity.firstName.charAt(0);
}
if (entity.lastName) {
initials += entity.lastName.charAt(0);
}
} else {
initials += entity.email.charAt(0);
}
return initials.toUpperCase();
}
getFullName(entity: User): string {
let fullName = '';
if ((entity.firstName && entity.firstName.length > 0) ||
(entity.lastName && entity.lastName.length > 0)) {
if (entity.firstName) {
fullName += entity.firstName;
}
if (entity.lastName) {
if (fullName.length > 0) {
fullName += ' ';
}
fullName += entity.lastName;
}
}
return fullName;
}
getAvatarBgColor(entity: User) {
return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60);
}
}

99
ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html

@ -28,63 +28,84 @@
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar> </mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content> <div mat-dialog-content style="padding: 24px 0 0 0">
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async" style="padding: 0 24px">
<div fxLayout="row" fxLayoutGap="6px"> <div fxLayout="row" fxLayoutGap="6px">
<mat-form-field class="mat-block"> <mat-form-field appearance="none" fxFlex class="mat-block">
<mat-label translate>alarm.created-time</mat-label>
<input matInput formControlName="createdTime" readonly>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.originator</mat-label> <mat-label translate>alarm.originator</mat-label>
<input matInput formControlName="originatorName" readonly> <input matInput formControlName="originatorName" readonly>
</mat-form-field> </mat-form-field>
</div> <mat-form-field appearance="none" fxFlex class="mat-block">
<div fxLayout="row" fxLayoutGap="6px" *ngIf="alarmFormGroup.get('startTime').value || <mat-label translate>alarm.created-time</mat-label>
alarmFormGroup.get('endTime').value"> <input matInput formControlName="createdTime" readonly>
<mat-form-field *ngIf="alarmFormGroup.get('startTime').value" fxFlex class="mat-block">
<mat-label translate>alarm.start-time</mat-label>
<input matInput formControlName="startTime" readonly>
</mat-form-field>
<mat-form-field *ngIf="alarmFormGroup.get('endTime').value" fxFlex class="mat-block">
<mat-label translate>alarm.end-time</mat-label>
<input matInput formControlName="endTime" readonly>
</mat-form-field>
<span fxFlex *ngIf="!alarmFormGroup.get('startTime').value || !alarmFormGroup.get('endTime').value"></span>
</div>
<div fxLayout="row" fxLayoutGap="6px" *ngIf="alarmFormGroup.get('ackTime').value ||
alarmFormGroup.get('clearTime').value">
<mat-form-field *ngIf="alarmFormGroup.get('ackTime').value" fxFlex class="mat-block">
<mat-label translate>alarm.ack-time</mat-label>
<input matInput formControlName="ackTime" readonly>
</mat-form-field>
<mat-form-field *ngIf="alarmFormGroup.get('clearTime').value" fxFlex class="mat-block">
<mat-label translate>alarm.clear-time</mat-label>
<input matInput formControlName="clearTime" readonly>
</mat-form-field> </mat-form-field>
<span fxFlex *ngIf="!alarmFormGroup.get('ackTime').value || !alarmFormGroup.get('clearTime').value"></span>
</div> </div>
<div fxLayout="row" fxLayoutGap="6px"> <div fxLayout="row" fxLayoutGap="6px">
<mat-form-field fxFlex class="mat-block"> <mat-form-field appearance="none" fxFlex class="mat-block">
<mat-label translate>alarm.type</mat-label> <mat-label translate>alarm.type</mat-label>
<input matInput formControlName="type" readonly> <input matInput formControlName="type" readonly>
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex class="mat-block"> <mat-form-field appearance="none" fxFlex class="mat-block">
<mat-label translate>alarm.severity</mat-label> <mat-label translate>alarm.severity</mat-label>
<input matInput formControlName="alarmSeverity" readonly <input matInput formControlName="alarmSeverity" readonly
[ngStyle]="{fontWeight: 'bold', color: alarmSeverityColorsMap.get((alarm$ | async)?.severity)}"> [ngStyle]="{fontWeight: 'bold', color: alarmSeverityColorsMap.get((alarm$ | async)?.severity)}">
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex class="mat-block"> </div>
<div fxLayout="row" fxLayoutGap="6px">
<mat-form-field appearance="none" fxFlex class="mat-block">
<mat-label translate>alarm.status</mat-label> <mat-label translate>alarm.status</mat-label>
<input matInput formControlName="alarmStatus" readonly> <input matInput formControlName="alarmStatus" readonly>
</mat-form-field> </mat-form-field>
<mat-form-field *ngIf="alarmFormGroup.get('assignee').value" appearance="none" fxFlex class="mat-block assignee-field">
<mat-label translate="">alarm.assignee</mat-label>
<span matPrefix fxLayout="row" fxLayoutAlign="center center" class="user-avatar"
[style.background-color]="getColorFromString(alarmFormGroup.get('assignee').value)">
{{ assigneeInitials }}
</span>
<input matInput formControlName="assignee" readonly>
</mat-form-field>
</div> </div>
<tb-json-object-edit <mat-expansion-panel class="tb-alarm-details">
*ngIf="displayDetails" <mat-expansion-panel-header fxLayout="row wrap">
formControlName="alarmDetails" <mat-panel-title>
readonly </mat-panel-title>
label="{{ 'alarm.details' | translate }}"> <mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
</tb-json-object-edit> alarm.advanced-info
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxLayout="row" fxLayoutGap="6px" *ngIf="alarmFormGroup.get('startTime').value ||
alarmFormGroup.get('endTime').value">
<mat-form-field *ngIf="alarmFormGroup.get('startTime').value" appearance="none" fxFlex class="mat-block">
<mat-label translate>alarm.start-time</mat-label>
<input matInput formControlName="startTime" readonly>
</mat-form-field>
<mat-form-field *ngIf="alarmFormGroup.get('endTime').value" appearance="none" fxFlex class="mat-block">
<mat-label translate>alarm.end-time</mat-label>
<input matInput formControlName="endTime" readonly>
</mat-form-field>
<span fxFlex *ngIf="!alarmFormGroup.get('startTime').value || !alarmFormGroup.get('endTime').value"></span>
</div>
<div fxLayout="row" fxLayoutGap="6px" *ngIf="alarmFormGroup.get('ackTime').value ||
alarmFormGroup.get('clearTime').value">
<mat-form-field *ngIf="alarmFormGroup.get('ackTime').value" appearance="none" fxFlex class="mat-block">
<mat-label translate>alarm.ack-time</mat-label>
<input matInput formControlName="ackTime" readonly>
</mat-form-field>
<mat-form-field *ngIf="alarmFormGroup.get('clearTime').value" appearance="none" fxFlex class="mat-block">
<mat-label translate>alarm.clear-time</mat-label>
<input matInput formControlName="clearTime" readonly>
</mat-form-field>
<span fxFlex *ngIf="!alarmFormGroup.get('ackTime').value || !alarmFormGroup.get('clearTime').value"></span>
</div>
<tb-json-object-edit
*ngIf="displayDetails"
formControlName="alarmDetails"
readonly
label="{{ 'alarm.details' | translate }}">
</tb-json-object-edit>
</ng-template>
</mat-expansion-panel>
</fieldset> </fieldset>
</div> </div>
<div mat-dialog-actions fxLayout="row"> <div mat-dialog-actions fxLayout="row">

61
ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.scss

@ -0,0 +1,61 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
.assignee-field {
.mat-form-field-label-wrapper {
overflow: visible;
left: -36px;
}
.user-avatar {
min-width: 28px;
min-height: 28px;
margin-right: 8px;
border-radius: 50%;
font-weight: 700;
color: #fff;
font-size: 13px;
line-height: normal;
}
}
.mat-expansion-panel {
&.tb-alarm-details {
box-shadow: none;
margin-bottom: 2px;
&.mat-expanded {
margin-bottom: 24px;
}
.mat-expansion-panel-header {
padding: 0;
height: 24px;
&:hover {
background: none !important;
}
.mat-expansion-indicator {
padding: 2px;
}
}
.mat-expansion-panel-header-description {
align-items: center;
}
.mat-expansion-panel-body {
padding: 0;
}
}
.mat-expansion-panel-content {
font: inherit;
}
}
}

53
ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts

@ -33,6 +33,7 @@ import { AlarmService } from '@core/http/alarm.service';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { UtilsService } from '@core/services/utils.service';
export interface AlarmDetailsDialogData { export interface AlarmDetailsDialogData {
alarmId?: string; alarmId?: string;
@ -45,7 +46,7 @@ export interface AlarmDetailsDialogData {
@Component({ @Component({
selector: 'tb-alarm-details-dialog', selector: 'tb-alarm-details-dialog',
templateUrl: './alarm-details-dialog.component.html', templateUrl: './alarm-details-dialog.component.html',
styleUrls: [] styleUrls: ['./alarm-details-dialog.component.scss']
}) })
export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDialogComponent, boolean> implements OnInit { export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDialogComponent, boolean> implements OnInit {
@ -66,6 +67,8 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
alarmUpdated = false; alarmUpdated = false;
assigneeInitials = '';
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
private datePipe: DatePipe, private datePipe: DatePipe,
@ -73,7 +76,8 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
@Inject(MAT_DIALOG_DATA) public data: AlarmDetailsDialogData, @Inject(MAT_DIALOG_DATA) public data: AlarmDetailsDialogData,
private alarmService: AlarmService, private alarmService: AlarmService,
public dialogRef: MatDialogRef<AlarmDetailsDialogComponent, boolean>, public dialogRef: MatDialogRef<AlarmDetailsDialogComponent, boolean>,
public fb: UntypedFormBuilder) { public fb: UntypedFormBuilder,
private utilsService: UtilsService) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.allowAcknowledgment = data.allowAcknowledgment; this.allowAcknowledgment = data.allowAcknowledgment;
@ -95,6 +99,7 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
assignTime: [''], assignTime: [''],
type: [''], type: [''],
alarmSeverity: [''], alarmSeverity: [''],
assignee: [''],
alarmStatus: [''], alarmStatus: [''],
alarmDetails: [null] alarmDetails: [null]
} }
@ -161,6 +166,11 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
.patchValue(this.translate.instant(alarmSeverityTranslations.get(alarm.severity))); .patchValue(this.translate.instant(alarmSeverityTranslations.get(alarm.severity)));
this.alarmFormGroup.get('alarmStatus') this.alarmFormGroup.get('alarmStatus')
.patchValue(this.translate.instant(alarmStatusTranslations.get(alarm.status))); .patchValue(this.translate.instant(alarmStatusTranslations.get(alarm.status)));
if (alarm.assigneeId) {
this.alarmFormGroup.get('assignee').
patchValue(this.getUserDisplayName(alarm));
this.assigneeInitials = this.getUserInitials(alarm);
}
this.alarmFormGroup.get('alarmDetails').patchValue(alarm.details); this.alarmFormGroup.get('alarmDetails').patchValue(alarm.details);
} }
@ -193,4 +203,43 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
} }
} }
getColorFromString(userDisplayName: string) {
return this.utilsService.stringToHslColor(userDisplayName, 40, 60);
}
getUserDisplayName(entity: AlarmInfo) {
let displayName = '';
if ((entity.assigneeFirstName && entity.assigneeFirstName.length > 0) ||
(entity.assigneeLastName && entity.assigneeLastName.length > 0)) {
if (entity.assigneeFirstName) {
displayName += entity.assigneeFirstName;
}
if (entity.assigneeLastName) {
if (displayName.length > 0) {
displayName += ' ';
}
displayName += entity.assigneeLastName;
}
} else {
displayName = entity.assigneeEmail;
}
return displayName;
}
getUserInitials(entity: AlarmInfo): string {
let initials = '';
if (entity.assigneeFirstName && entity.assigneeFirstName.length ||
entity.assigneeLastName && entity.assigneeLastName.length) {
if (entity.assigneeFirstName) {
initials += entity.assigneeFirstName.charAt(0);
}
if (entity.assigneeLastName) {
initials += entity.assigneeLastName.charAt(0);
}
} else {
initials += entity.assigneeEmail.charAt(0);
}
return initials.toUpperCase();
}
} }

145
ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts

@ -15,6 +15,7 @@
/// ///
import { import {
CellActionDescriptorType,
DateEntityTableColumn, DateEntityTableColumn,
EntityTableColumn, EntityTableColumn,
EntityTableConfig EntityTableConfig
@ -48,6 +49,15 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { ChangeDetectorRef, Injector, StaticProvider, ViewContainerRef } from '@angular/core';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import {
ALARM_ASSIGNEE_PANEL_DATA, AlarmAssigneePanelComponent,
AlarmAssigneePanelData
} from '@home/components/alarm/alarm-assignee-panel.component';
import { ComponentPortal } from '@angular/cdk/portal';
import { isDefinedAndNotNull } from '@core/utils';
import { UtilsService } from '@core/services/utils.service';
export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink> { export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink> {
@ -62,7 +72,11 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
private dialog: MatDialog, private dialog: MatDialog,
public entityId: EntityId = null, public entityId: EntityId = null,
private defaultSearchStatus: AlarmSearchStatus = AlarmSearchStatus.ANY, private defaultSearchStatus: AlarmSearchStatus = AlarmSearchStatus.ANY,
private store: Store<AppState>) { private store: Store<AppState>,
private viewContainerRef: ViewContainerRef,
private overlay: Overlay,
private cd: ChangeDetectorRef,
private utilsService: UtilsService) {
super(); super();
this.loadDataOnInit = false; this.loadDataOnInit = false;
this.tableTitle = ''; this.tableTitle = '';
@ -97,10 +111,35 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
this.columns.push( this.columns.push(
new EntityTableColumn<AlarmInfo>('severity', 'alarm.severity', '25%', new EntityTableColumn<AlarmInfo>('severity', 'alarm.severity', '25%',
(entity) => this.translate.instant(alarmSeverityTranslations.get(entity.severity)), (entity) => this.translate.instant(alarmSeverityTranslations.get(entity.severity)),
entity => ({ entity => ({
fontWeight: 'bold', fontWeight: 'bold',
color: alarmSeverityColors.get(entity.severity) color: alarmSeverityColors.get(entity.severity)
}))); })));
this.columns.push(
new EntityTableColumn<AlarmInfo>('assigneeEmail', 'alarm.assignee', '200px',
(entity) => {
return this.getAssigneeTemplate(entity)
},
(entity) => {
return {
display: 'flex',
justifyContent: 'start',
alignItems: 'center',
height: 'inherit'
}
},
false,
() => ({}),
(entity) => undefined,
false,
{
icon: 'keyboard_arrow_down',
type: CellActionDescriptorType.DEFAULT,
isEnabled: (entity) => true,
name: this.translate.instant('alarm.assign'),
onAction: ($event, entity) => this.openAlarmAssigneePanel($event, entity)
})
)
this.columns.push( this.columns.push(
new EntityTableColumn<AlarmInfo>('status', 'alarm.status', '25%', new EntityTableColumn<AlarmInfo>('status', 'alarm.status', '25%',
(entity) => this.translate.instant(alarmStatusTranslations.get(entity.status)))); (entity) => this.translate.instant(alarmStatusTranslations.get(entity.status))));
@ -142,4 +181,100 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
} }
); );
} }
getAssigneeTemplate(entity: AlarmInfo): string {
return `
<span class="assignee-cell">
${isDefinedAndNotNull(entity.assigneeId) ?
`<span class="assigned-container">
<span class="user-avatar" style="background-color: ${this.getAvatarBgColor(entity)}">
${this.getUserInitials(entity)}
</span>
<span>${this.getUserDisplayName(entity)}</span>
</span>`
:
`<mat-icon class="material-icons unassigned-icon">account_circle</mat-icon>
<span>${this.translate.instant('alarm.unassigned')}</span>`
}
</span>`
}
getUserDisplayName(entity: AlarmInfo) {
let displayName = '';
if ((entity.assigneeFirstName && entity.assigneeFirstName.length > 0) ||
(entity.assigneeLastName && entity.assigneeLastName.length > 0)) {
if (entity.assigneeFirstName) {
displayName += entity.assigneeFirstName;
}
if (entity.assigneeLastName) {
if (displayName.length > 0) {
displayName += ' ';
}
displayName += entity.assigneeLastName;
}
} else {
displayName = entity.assigneeEmail;
}
return displayName;
}
getUserInitials(entity: AlarmInfo): string {
let initials = '';
if (entity.assigneeFirstName && entity.assigneeFirstName.length ||
entity.assigneeLastName && entity.assigneeLastName.length) {
if (entity.assigneeFirstName) {
initials += entity.assigneeFirstName.charAt(0);
}
if (entity.assigneeLastName) {
initials += entity.assigneeLastName.charAt(0);
}
} else {
initials += entity.assigneeEmail.charAt(0);
}
return initials.toUpperCase();
}
getAvatarBgColor(entity: AlarmInfo) {
return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60);
}
openAlarmAssigneePanel($event: Event, entity: AlarmInfo) {
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'center',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
config.minWidth = '260px';
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const providers: StaticProvider[] = [
{
provide: ALARM_ASSIGNEE_PANEL_DATA,
useValue: {
alarmId: entity.id.id,
assigneeId: entity.assigneeId?.id
} as AlarmAssigneePanelData
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(AlarmAssigneePanelComponent,
this.viewContainerRef, injector)).onDestroy(() => this.updateData());
}
} }

27
ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss

@ -17,6 +17,33 @@
tb-entities-table { tb-entities-table {
.mat-drawer-container { .mat-drawer-container {
background-color: white; background-color: white;
.assignee-cell {
display: flex;
justify-content: flex-start;
align-items: center;
.assigned-container {
.user-avatar {
display: inline-flex;
justify-content: center;
align-items: center;
margin-right: 8px;
border-radius: 50%;
width: 28px;
height: 28px;
color: white;
font-size: 13px;
font-weight: 700;
}
}
.material-icons.unassigned-icon {
width: 28px;
height: 28px;
font-size: 28px;
margin-right: 8px;
color: rgba(0, 0, 0, 0.38);
overflow: visible;
}
}
} }
} }
} }

16
ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { ChangeDetectorRef, Component, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
@ -26,6 +26,8 @@ import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { AlarmService } from '@app/core/http/alarm.service'; import { AlarmService } from '@app/core/http/alarm.service';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Overlay } from '@angular/cdk/overlay';
import { UtilsService } from '@core/services/utils.service';
@Component({ @Component({
selector: 'tb-alarm-table', selector: 'tb-alarm-table',
@ -71,7 +73,11 @@ export class AlarmTableComponent implements OnInit {
private translate: TranslateService, private translate: TranslateService,
private datePipe: DatePipe, private datePipe: DatePipe,
private dialog: MatDialog, private dialog: MatDialog,
private store: Store<AppState>) { private store: Store<AppState>,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef,
private utilsService: UtilsService) {
} }
ngOnInit() { ngOnInit() {
@ -84,7 +90,11 @@ export class AlarmTableComponent implements OnInit {
this.dialog, this.dialog,
this.entityIdValue, this.entityIdValue,
AlarmSearchStatus.ANY, AlarmSearchStatus.ANY,
this.store this.store,
this.viewContainerRef,
this.overlay,
this.cd,
this.utilsService
); );
} }

3
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -175,6 +175,7 @@ import { AssetProfileDialogComponent } from '@home/components/profile/asset-prof
import { AssetProfileAutocompleteComponent } from '@home/components/profile/asset-profile-autocomplete.component'; import { AssetProfileAutocompleteComponent } from '@home/components/profile/asset-profile-autocomplete.component';
import { MODULES_MAP } from '@shared/models/constants'; import { MODULES_MAP } from '@shared/models/constants';
import { modulesMap } from '@modules/common/modules-map'; import { modulesMap } from '@modules/common/modules-map';
import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -197,6 +198,7 @@ import { modulesMap } from '@modules/common/modules-map';
RelationFiltersComponent, RelationFiltersComponent,
AlarmTableHeaderComponent, AlarmTableHeaderComponent,
AlarmTableComponent, AlarmTableComponent,
AlarmAssigneePanelComponent,
AttributeTableComponent, AttributeTableComponent,
AddAttributeDialogComponent, AddAttributeDialogComponent,
EditAttributeValuePanelComponent, EditAttributeValuePanelComponent,
@ -344,6 +346,7 @@ import { modulesMap } from '@modules/common/modules-map';
RelationTableComponent, RelationTableComponent,
RelationFiltersComponent, RelationFiltersComponent,
AlarmTableComponent, AlarmTableComponent,
AlarmAssigneePanelComponent,
AttributeTableComponent, AttributeTableComponent,
AliasesEntitySelectComponent, AliasesEntitySelectComponent,
AliasesEntityAutocompleteComponent, AliasesEntityAutocompleteComponent,

24
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html

@ -82,8 +82,30 @@
{{ column.title }} {{ column.title }}
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let alarm; let row = index" <mat-cell *matCellDef="let alarm; let row = index"
[innerHTML]="cellContent(alarm, column, row)"
[ngStyle]="cellStyle(alarm, column, row)"> [ngStyle]="cellStyle(alarm, column, row)">
<span [innerHTML]="cellContent(alarm, column, row)"></span>
<ng-container *ngIf="column.entityKey.key === 'assigneeEmail'">
<span class="assignee-cell" fxLayout="row" fxLayoutAlign="start center">
<span *ngIf="alarm.assigneeId" class="assigned-container">
<span class="user-avatar" [style.backgroundColor]="getAvatarBgColor(alarm)">
{{ getUserInitials(alarm) }}
</span>
<span style="text-overflow: ellipsis">{{ getUserDisplayName(alarm) }}</span>
</span>
<span *ngIf="!alarm.assigneeId" class="unassigned-container" fxLayout="row" fxLayoutAlign="start center">
<mat-icon class="material-icons unassigned-icon">account_circle</mat-icon>
<span translate>alarm.unassigned</span>
</span>
<button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ alarm.assign | translate }}"
matTooltipPosition="above"
(click)="openAlarmAssigneePanel($event, alarm)">
<mat-icon>
keyboard_arrow_down
</mat-icon>
</button>
</span>
</ng-container>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction"> <ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction">

36
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host { :host::ng-deep {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block; display: block;
@ -26,6 +26,40 @@
&.invisible { &.invisible {
visibility: hidden; visibility: hidden;
} }
.mat-cell {
.assignee-cell {
.assigned-container {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.user-avatar {
display: inline-flex;
justify-content: center;
align-items: center;
margin-right: 8px;
border-radius: 50%;
width: 28px;
height: 28px;
color: white;
font-size: 13px;
font-weight: 700;
}
}
.unassigned-container {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.material-icons.unassigned-icon {
width: 28px;
height: 28px;
font-size: 28px;
margin-right: 8px;
color: rgba(0, 0, 0, 0.38);
overflow: visible;
}
}
}
}
} }
} }
span.no-data-found { span.no-data-found {

92
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts

@ -93,7 +93,7 @@ import {
} from '@home/components/widget/lib/display-columns-panel.component'; } from '@home/components/widget/lib/display-columns-panel.component';
import { import {
AlarmDataInfo, AlarmDataInfo,
alarmFields, alarmFields, AlarmInfo,
AlarmSearchStatus, AlarmSearchStatus,
alarmSeverityColors, alarmSeverityColors,
alarmSeverityTranslations, alarmSeverityTranslations,
@ -127,6 +127,10 @@ import { entityFields } from '@shared/models/entity.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants'; import { hidePageSizePixelValue } from '@shared/models/constants';
import {
ALARM_ASSIGNEE_PANEL_DATA, AlarmAssigneePanelComponent,
AlarmAssigneePanelData
} from '@home/components/alarm/alarm-assignee-panel.component';
interface AlarmsTableWidgetSettings extends TableWidgetSettings { interface AlarmsTableWidgetSettings extends TableWidgetSettings {
alarmsTitle: string; alarmsTitle: string;
@ -418,6 +422,9 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if (alarmField && alarmField.time) { if (alarmField && alarmField.time) {
keySettings.columnWidth = '120px'; keySettings.columnWidth = '120px';
} }
if (alarmField && alarmField.keyName === alarmFields.assigneeEmail.keyName) {
keySettings.columnWidth = '120px'
}
} }
this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings, 'value, alarm, ctx'); this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings, 'value, alarm, ctx');
this.contentsInfo[dataKey.def] = getCellContentInfo(keySettings, 'value, alarm, ctx'); this.contentsInfo[dataKey.def] = getCellContentInfo(keySettings, 'value, alarm, ctx');
@ -966,7 +973,10 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
return alarmStatusTranslations.get(value) ? this.translate.instant(alarmStatusTranslations.get(value)) : value; return alarmStatusTranslations.get(value) ? this.translate.instant(alarmStatusTranslations.get(value)) : value;
} else if (alarmField.value === alarmFields.originatorType.value) { } else if (alarmField.value === alarmFields.originatorType.value) {
return this.translate.instant(entityTypeTranslations.get(value).type); return this.translate.instant(entityTypeTranslations.get(value).type);
} else { } else if (alarmField.value === alarmFields.assigneeEmail.value) {
return '';
}
else {
return value; return value;
} }
} }
@ -1013,6 +1023,84 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.cellStyleCache.length = 0; this.cellStyleCache.length = 0;
this.rowStyleCache.length = 0; this.rowStyleCache.length = 0;
} }
getUserDisplayName(entity: AlarmInfo) {
let displayName = '';
if ((entity.assigneeFirstName && entity.assigneeFirstName.length > 0) ||
(entity.assigneeLastName && entity.assigneeLastName.length > 0)) {
if (entity.assigneeFirstName) {
displayName += entity.assigneeFirstName;
}
if (entity.assigneeLastName) {
if (displayName.length > 0) {
displayName += ' ';
}
displayName += entity.assigneeLastName;
}
} else {
displayName = entity.assigneeEmail;
}
return displayName;
}
getUserInitials(entity: AlarmInfo): string {
let initials = '';
if (entity.assigneeFirstName && entity.assigneeFirstName.length ||
entity.assigneeLastName && entity.assigneeLastName.length) {
if (entity.assigneeFirstName) {
initials += entity.assigneeFirstName.charAt(0);
}
if (entity.assigneeLastName) {
initials += entity.assigneeLastName.charAt(0);
}
} else {
initials += entity.assigneeEmail.charAt(0);
}
return initials.toUpperCase();
}
getAvatarBgColor(entity: AlarmInfo) {
return this.utils.stringToHslColor(this.getUserDisplayName(entity), 40, 60);
}
openAlarmAssigneePanel($event: Event, entity: AlarmInfo) {
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'center',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
config.minWidth = '260px';
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const providers: StaticProvider[] = [
{
provide: ALARM_ASSIGNEE_PANEL_DATA,
useValue: {
alarmId: entity.id.id,
assigneeId: entity.assigneeId?.id
} as AlarmAssigneePanelData
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(AlarmAssigneePanelComponent,
this.viewContainerRef, injector));
}
} }
class AlarmsDatasource implements DataSource<AlarmDataInfo> { class AlarmsDatasource implements DataSource<AlarmDataInfo> {

13
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -440,9 +440,19 @@
"assignee-last-name": "Assignee last name", "assignee-last-name": "Assignee last name",
"assignee-email": "Assignee email", "assignee-email": "Assignee email",
"details": "Details", "details": "Details",
"originator-label": "Originator label",
"assign": "Assign",
"assignments": "Assignments",
"assignee": "Assignee",
"assignee-id": "Assignee id",
"assignee-first-name": "Assignee first name",
"assignee-last-name": "Assignee last name",
"assignee-email": "Assignee email",
"unassigned": "Unassigned",
"status": "Status", "status": "Status",
"alarm-details": "Alarm details", "alarm-details": "Alarm details",
"start-time": "Start time", "start-time": "Start time",
"assign-time": "Assign time",
"end-time": "End time", "end-time": "End time",
"ack-time": "Acknowledged time", "ack-time": "Acknowledged time",
"clear-time": "Cleared time", "clear-time": "Cleared time",
@ -480,7 +490,8 @@
"fetch-size-error-min": "Minimum value is 10.", "fetch-size-error-min": "Minimum value is 10.",
"alarm-type-list": "Alarm type list", "alarm-type-list": "Alarm type list",
"any-type": "Any type", "any-type": "Any type",
"search-propagated-alarms": "Search propagated alarms" "search-propagated-alarms": "Search propagated alarms",
"advanced-info": "Advanced info"
}, },
"alias": { "alias": {
"add": "Add alias", "add": "Add alias",

Loading…
Cancel
Save