From 994971a5cd72dc5de9945bc00e9c4ed93c78a4c4 Mon Sep 17 00:00:00 2001 From: rusikv Date: Thu, 16 Feb 2023 14:51:24 +0200 Subject: [PATCH] UI alarm assignment implementation --- ui-ngx/src/app/core/http/alarm.service.ts | 8 + ui-ngx/src/app/core/services/utils.service.ts | 9 +- .../alarm/alarm-assignee-panel.component.html | 51 ++++ .../alarm/alarm-assignee-panel.component.scss | 101 ++++++++ .../alarm/alarm-assignee-panel.component.ts | 227 ++++++++++++++++++ .../alarm/alarm-details-dialog.component.html | 99 +++++--- .../alarm/alarm-details-dialog.component.scss | 61 +++++ .../alarm/alarm-details-dialog.component.ts | 53 +++- .../components/alarm/alarm-table-config.ts | 145 ++++++++++- .../alarm/alarm-table.component.scss | 27 +++ .../components/alarm/alarm-table.component.ts | 16 +- .../home/components/home-components.module.ts | 3 + .../lib/alarms-table-widget.component.html | 24 +- .../lib/alarms-table-widget.component.scss | 36 ++- .../lib/alarms-table-widget.component.ts | 92 ++++++- .../assets/locale/locale.constant-en_US.json | 13 +- 16 files changed, 910 insertions(+), 55 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.scss diff --git a/ui-ngx/src/app/core/http/alarm.service.ts b/ui-ngx/src/app/core/http/alarm.service.ts index 802045b684..076f590199 100644 --- a/ui-ngx/src/app/core/http/alarm.service.ts +++ b/ui-ngx/src/app/core/http/alarm.service.ts @@ -60,6 +60,14 @@ export class AlarmService { return this.http.post(`/api/alarm/${alarmId}/clear`, null, defaultHttpOptionsFromConfig(config)); } + public assignAlarm(alarmId: string, assigneeId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/alarm/${alarmId}/assign/${assigneeId}`, null, defaultHttpOptionsFromConfig(config)); + } + + public unassignAlarm(alarmId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/alarm/${alarmId}/assign`, defaultHttpOptionsFromConfig(config)); + } + public deleteAlarm(alarmId: string, config?: RequestConfig): Observable { return this.http.delete(`/api/alarm/${alarmId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index ce6cf32dbd..ea4c85eb64 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/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(); diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html new file mode 100644 index 0000000000..262a45e435 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html @@ -0,0 +1,51 @@ + + + + search + + + account_circle + alarm.unassigned + + + + +
+ + +
+
+ + + {{ translate.get('user.no-users-matching', {entity: searchText}) | async }} + + +
+
+ diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.scss new file mode 100644 index 0000000000..462df2e0d2 --- /dev/null +++ b/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; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts new file mode 100644 index 0000000000..910a6f57c0 --- /dev/null +++ b/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('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>; + + searchText = ''; + + private destroy$ = new Subject(); + + 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> { + 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())), + 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); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html index 3bdb10db57..eb67fe32a6 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html @@ -28,63 +28,84 @@
-
-
+
+
- - alarm.created-time - - - + alarm.originator -
-
- - alarm.start-time - - - - alarm.end-time - - - -
-
- - alarm.ack-time - - - - alarm.clear-time - + + alarm.created-time + -
- + alarm.type - + alarm.severity - +
+
+ alarm.status + + alarm.assignee + + {{ assigneeInitials }} + + +
- - + + + + + + alarm.advanced-info + + + +
+ + alarm.start-time + + + + alarm.end-time + + + +
+
+ + alarm.ack-time + + + + alarm.clear-time + + + +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.scss new file mode 100644 index 0000000000..76148a1dd5 --- /dev/null +++ b/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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts index 35db88231d..2e97ce7405 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts +++ b/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 implements OnInit { @@ -66,6 +67,8 @@ export class AlarmDetailsDialogComponent extends DialogComponent, protected router: Router, private datePipe: DatePipe, @@ -73,7 +76,8 @@ export class AlarmDetailsDialogComponent extends DialogComponent, - 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 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(); + } + } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts index 669be64636..f29dae1926 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts +++ b/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 { @@ -62,7 +72,11 @@ export class AlarmTableConfig extends EntityTableConfig private dialog: MatDialog, public entityId: EntityId = null, private defaultSearchStatus: AlarmSearchStatus = AlarmSearchStatus.ANY, - private store: Store) { + private store: Store, + 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 this.columns.push( new EntityTableColumn('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('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('status', 'alarm.status', '25%', (entity) => this.translate.instant(alarmStatusTranslations.get(entity.status)))); @@ -142,4 +181,100 @@ export class AlarmTableConfig extends EntityTableConfig } ); } + + getAssigneeTemplate(entity: AlarmInfo): string { + return ` + + ${isDefinedAndNotNull(entity.assigneeId) ? + ` + + ${this.getUserInitials(entity)} + + ${this.getUserDisplayName(entity)} + ` + : + `account_circle + ${this.translate.instant('alarm.unassigned')}` + } + ` + } + + 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()); + } + } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss index b80526aee8..b6dcfdecea 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss +++ b/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; + } + } } } } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts index 0d53eaaf42..5885c38db0 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts +++ b/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) { + private store: Store, + 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 ); } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 99d30c70cd..917f608f94 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/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, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html index 9e431c1ccd..def95c93f9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html @@ -82,8 +82,30 @@ {{ column.title }} + + + + + + {{ getUserInitials(alarm) }} + + {{ getUserDisplayName(alarm) }} + + + account_circle + alarm.unassigned + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss index 1ee9b509f2..aecef669a4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss +++ b/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 { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index 35392e4f9e..df2e9c634b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/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 { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 935cd507ca..2e0be36b3f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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",