Browse Source

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

[WIP][3.5] UI: Alarm assignment
pull/8102/head
Andrew Shvayka 3 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));
}
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> {
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,
deepClone,
deleteNullProperties,
guid,
guid, hashCode,
isDefined,
isDefinedAndNotNull,
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 {
return this.window.performance && this.window.performance.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>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div mat-dialog-content style="padding: 24px 0 0 0">
<fieldset [disabled]="isLoading$ | async" style="padding: 0 24px">
<div fxLayout="row" fxLayoutGap="6px">
<mat-form-field 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-form-field appearance="none" fxFlex class="mat-block">
<mat-label translate>alarm.originator</mat-label>
<input matInput formControlName="originatorName" readonly>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="6px" *ngIf="alarmFormGroup.get('startTime').value ||
alarmFormGroup.get('endTime').value">
<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 appearance="none" fxFlex class="mat-block">
<mat-label translate>alarm.created-time</mat-label>
<input matInput formControlName="createdTime" readonly>
</mat-form-field>
<span fxFlex *ngIf="!alarmFormGroup.get('ackTime').value || !alarmFormGroup.get('clearTime').value"></span>
</div>
<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>
<input matInput formControlName="type" readonly>
</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>
<input matInput formControlName="alarmSeverity" readonly
[ngStyle]="{fontWeight: 'bold', color: alarmSeverityColorsMap.get((alarm$ | async)?.severity)}">
</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>
<input matInput formControlName="alarmStatus" readonly>
</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>
<tb-json-object-edit
*ngIf="displayDetails"
formControlName="alarmDetails"
readonly
label="{{ 'alarm.details' | translate }}">
</tb-json-object-edit>
<mat-expansion-panel class="tb-alarm-details">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
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>
</div>
<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 { DatePipe } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { UtilsService } from '@core/services/utils.service';
export interface AlarmDetailsDialogData {
alarmId?: string;
@ -45,7 +46,7 @@ export interface AlarmDetailsDialogData {
@Component({
selector: 'tb-alarm-details-dialog',
templateUrl: './alarm-details-dialog.component.html',
styleUrls: []
styleUrls: ['./alarm-details-dialog.component.scss']
})
export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDialogComponent, boolean> implements OnInit {
@ -66,6 +67,8 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
alarmUpdated = false;
assigneeInitials = '';
constructor(protected store: Store<AppState>,
protected router: Router,
private datePipe: DatePipe,
@ -73,7 +76,8 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
@Inject(MAT_DIALOG_DATA) public data: AlarmDetailsDialogData,
private alarmService: AlarmService,
public dialogRef: MatDialogRef<AlarmDetailsDialogComponent, boolean>,
public fb: UntypedFormBuilder) {
public fb: UntypedFormBuilder,
private utilsService: UtilsService) {
super(store, router, dialogRef);
this.allowAcknowledgment = data.allowAcknowledgment;
@ -95,6 +99,7 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
assignTime: [''],
type: [''],
alarmSeverity: [''],
assignee: [''],
alarmStatus: [''],
alarmDetails: [null]
}
@ -161,6 +166,11 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
.patchValue(this.translate.instant(alarmSeverityTranslations.get(alarm.severity)));
this.alarmFormGroup.get('alarmStatus')
.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);
}
@ -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 {
CellActionDescriptorType,
DateEntityTableColumn,
EntityTableColumn,
EntityTableConfig
@ -48,6 +49,15 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
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> {
@ -62,7 +72,11 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
private dialog: MatDialog,
public entityId: EntityId = null,
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();
this.loadDataOnInit = false;
this.tableTitle = '';
@ -97,10 +111,35 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
this.columns.push(
new EntityTableColumn<AlarmInfo>('severity', 'alarm.severity', '25%',
(entity) => this.translate.instant(alarmSeverityTranslations.get(entity.severity)),
entity => ({
fontWeight: 'bold',
color: alarmSeverityColors.get(entity.severity)
})));
entity => ({
fontWeight: 'bold',
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(
new EntityTableColumn<AlarmInfo>('status', 'alarm.status', '25%',
(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 {
.mat-drawer-container {
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.
///
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 { DatePipe } from '@angular/common';
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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Overlay } from '@angular/cdk/overlay';
import { UtilsService } from '@core/services/utils.service';
@Component({
selector: 'tb-alarm-table',
@ -71,7 +73,11 @@ export class AlarmTableComponent implements OnInit {
private translate: TranslateService,
private datePipe: DatePipe,
private dialog: MatDialog,
private store: Store<AppState>) {
private store: Store<AppState>,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef,
private utilsService: UtilsService) {
}
ngOnInit() {
@ -84,7 +90,11 @@ export class AlarmTableComponent implements OnInit {
this.dialog,
this.entityIdValue,
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 { MODULES_MAP } from '@shared/models/constants';
import { modulesMap } from '@modules/common/modules-map';
import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component';
@NgModule({
declarations:
@ -197,6 +198,7 @@ import { modulesMap } from '@modules/common/modules-map';
RelationFiltersComponent,
AlarmTableHeaderComponent,
AlarmTableComponent,
AlarmAssigneePanelComponent,
AttributeTableComponent,
AddAttributeDialogComponent,
EditAttributeValuePanelComponent,
@ -344,6 +346,7 @@ import { modulesMap } from '@modules/common/modules-map';
RelationTableComponent,
RelationFiltersComponent,
AlarmTableComponent,
AlarmAssigneePanelComponent,
AttributeTableComponent,
AliasesEntitySelectComponent,
AliasesEntityAutocompleteComponent,

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

@ -82,8 +82,30 @@
{{ column.title }}
</mat-header-cell>
<mat-cell *matCellDef="let alarm; let row = index"
[innerHTML]="cellContent(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>
</ng-container>
<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
* limitations under the License.
*/
:host {
:host::ng-deep {
width: 100%;
height: 100%;
display: block;
@ -26,6 +26,40 @@
&.invisible {
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 {

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';
import {
AlarmDataInfo,
alarmFields,
alarmFields, AlarmInfo,
AlarmSearchStatus,
alarmSeverityColors,
alarmSeverityTranslations,
@ -127,6 +127,10 @@ import { entityFields } from '@shared/models/entity.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ResizeObserver } from '@juggle/resize-observer';
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 {
alarmsTitle: string;
@ -418,6 +422,9 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if (alarmField && alarmField.time) {
keySettings.columnWidth = '120px';
}
if (alarmField && alarmField.keyName === alarmFields.assigneeEmail.keyName) {
keySettings.columnWidth = '120px'
}
}
this.stylesInfo[dataKey.def] = getCellStyleInfo(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;
} else if (alarmField.value === alarmFields.originatorType.value) {
return this.translate.instant(entityTypeTranslations.get(value).type);
} else {
} else if (alarmField.value === alarmFields.assigneeEmail.value) {
return '';
}
else {
return value;
}
}
@ -1013,6 +1023,84 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.cellStyleCache.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> {

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

@ -440,9 +440,19 @@
"assignee-last-name": "Assignee last name",
"assignee-email": "Assignee email",
"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",
"alarm-details": "Alarm details",
"start-time": "Start time",
"assign-time": "Assign time",
"end-time": "End time",
"ack-time": "Acknowledged time",
"clear-time": "Cleared time",
@ -480,7 +490,8 @@
"fetch-size-error-min": "Minimum value is 10.",
"alarm-type-list": "Alarm type list",
"any-type": "Any type",
"search-propagated-alarms": "Search propagated alarms"
"search-propagated-alarms": "Search propagated alarms",
"advanced-info": "Advanced info"
},
"alias": {
"add": "Add alias",

Loading…
Cancel
Save