Browse Source

Merge pull request #7942 from rusikv/alarmCommentsUI

[3.5] UI: Alarm comments feature
pull/8145/head
Andrew Shvayka 4 years ago
committed by GitHub
parent
commit
b93e34f79f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      application/src/main/data/json/demo/dashboards/thermostats.json
  2. 2
      application/src/main/data/json/system/widget_bundles/alarm_widgets.json
  3. 46
      ui-ngx/src/app/core/http/alarm-comment.service.ts
  4. 8
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html
  5. 12
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.scss
  6. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts
  7. 41
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html
  8. 51
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss
  9. 141
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts
  10. 55
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html
  11. 64
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts
  12. 161
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html
  13. 107
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.scss
  14. 295
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts
  15. 13
      ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html
  16. 69
      ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts
  17. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  18. 13
      ui-ngx/src/app/modules/home/components/shared-home-components.module.ts
  19. 40
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  20. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html
  21. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts
  22. 25
      ui-ngx/src/app/shared/models/alarm.models.ts
  23. 24
      ui-ngx/src/app/shared/models/id/alarm-comment-id.ts
  24. 58
      ui-ngx/src/app/shared/pipe/date-ago.pipe.ts
  25. 8
      ui-ngx/src/app/shared/shared.module.ts
  26. 26
      ui-ngx/src/assets/locale/locale.constant-en_US.json

10
application/src/main/data/json/demo/dashboards/thermostats.json

@ -215,6 +215,8 @@
"displayDetails": true,
"allowAcknowledgment": true,
"allowClear": true,
"allowAssign": true,
"displayComments": true,
"displayPagination": true,
"defaultPageSize": 10,
"defaultSortOrder": "-createdTime",
@ -277,6 +279,14 @@
"color": "#607d8b",
"settings": {},
"_hash": 0.7977920750136249
},
{
"name": "assignee",
"type": "alarm",
"label": "Assignee",
"color": "#9c27b0",
"settings": {},
"_hash": 0.8678751039018493
}
]
},

2
application/src/main/data/json/system/widget_bundles/alarm_widgets.json

@ -23,7 +23,7 @@
"dataKeySettingsSchema": "",
"settingsDirective": "tb-alarms-table-widget-settings",
"dataKeySettingsDirective": "tb-alarms-table-key-settings",
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false}"
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"allowAssign\":true,\"displayComments\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false}"
}
}
]

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

@ -0,0 +1,46 @@
///
/// 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 { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
import { AlarmComment, AlarmCommentInfo } from '@shared/models/alarm.models';
@Injectable({
providedIn: 'root'
})
export class AlarmCommentService {
constructor(
private http: HttpClient
) { }
public saveAlarmComment(alarmId: string, alarmComment: AlarmComment, config?: RequestConfig): Observable<AlarmComment> {
return this.http.post<AlarmComment>(`/api/alarm/${alarmId}/comment`, alarmComment, defaultHttpOptionsFromConfig(config));
}
public getAlarmComments(alarmId: string, pageLink: PageLink, config?: RequestConfig): Observable<PageData<AlarmCommentInfo>> {
return this.http.get<PageData<AlarmComment>>(`/api/alarm/${alarmId}/comment${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config));
}
public deleteAlarmComments(alarmId: string, commentId: string, config?: RequestConfig): Observable<void> {
return this.http.delete<void>(`/api/alarm/${alarmId}/comment/${commentId}`, defaultHttpOptionsFromConfig(config));
}
}

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

@ -27,12 +27,12 @@
[displayWith]="displayUserFn"
(optionSelected)="selected($event)"
panelWidth="260px">
<mat-option [ngClass]="{'assigned': !assigneeId}" [value]="null">
<mat-option [fxHide]="!assigneeId" [value]="null">
<mat-icon class="unassigned-icon">account_circle</mat-icon>
<span style="margin-right: auto" translate>alarm.unassigned</span>
<span translate>alarm.unassigned</span>
</mat-option>
<mat-option [ngClass]="{ 'assigned': assigneeId === user.id.id }"
*ngFor="let user of filteredUsers | async" [value]="user">
<mat-option *ngFor="let user of filteredUsers | async" [value]="user"
[fxHide]="assigneeId === user.id.id">
<span class="user-avatar" [innerHTML]="getUserInitials(user)"
[style.background-color]="getAvatarBgColor(user)">
</span>

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

@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
overflow: auto;
@ -26,7 +27,6 @@
padding-top: 8px;
height: 340px;
font-size: 14px;
background-color: #fff;
.mat-form-field-wrapper {
margin-right: 8px;
@ -44,17 +44,12 @@
}
.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;
@ -70,7 +65,6 @@
display: inline-flex;
justify-content: center;
align-items: center;
align-self: center;
margin-right: 8px;
border-radius: 50%;
background-color: #5cb445;
@ -78,14 +72,14 @@
height: 28px;
min-width: 28px;
min-height: 28px;
color: white;
color: #fff;
font-size: 13px;
font-weight: 700
}
.user-display-name {
max-width: 180px;
overflow: hidden;
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

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

@ -119,7 +119,7 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
}
selected(event: MatAutocompleteSelectedEvent): void {
this.selectUserFormGroup.get('user').patchValue('');
this.clear();
const user: User = event.option.value;
if (user) {
this.assign(user);

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

@ -0,0 +1,41 @@
<!--
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.
-->
<div class="tb-assignee" fxLayout="row" fxLayoutAlign="start center"
(click)="openAlarmAssigneePanel($event, alarm)">
<span *ngIf="alarm.assigneeId" class="assigned-container">
<span class="user-avatar" [style.backgroundColor]="getAvatarBgColor(alarm.assignee)">
{{ getUserInitials(alarm.assignee) }}
</span>
<span [matTooltip]="getUserDisplayName(alarm.assignee)"
matTooltipPosition="above"
style="text-overflow: ellipsis">{{ getUserDisplayName(alarm.assignee) }}</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 fxFlexAlign="flex-end"
mat-icon-button
matTooltip="{{ 'alarm.assign' | translate }}"
matTooltipPosition="above">
<mat-icon>
keyboard_arrow_down
</mat-icon>
</button>
</div>

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

@ -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.
*/
:host {
.tb-assignee {
cursor: pointer;
.assigned-container {
max-width: 233px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.user-avatar {
display: inline-flex;
justify-content: center;
align-items: center;
margin-right: 8px;
border-radius: 50%;
width: 28px;
height: 28px;
min-width: 28px;
min-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;
}
}
}

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

@ -0,0 +1,141 @@
///
/// 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 {
Component, EventEmitter, Injector, Input, Output, StaticProvider, ViewContainerRef
} from '@angular/core';
import { UtilsService } from '@core/services/utils.service';
import { AlarmAssignee, AlarmInfo } from '@shared/models/alarm.models';
import {
ALARM_ASSIGNEE_PANEL_DATA, AlarmAssigneePanelComponent,
AlarmAssigneePanelData
} from '@home/components/alarm/alarm-assignee-panel.component';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
@Component({
selector: 'tb-alarm-assignee',
templateUrl: './alarm-assignee.component.html',
styleUrls: ['./alarm-assignee.component.scss']
})
export class AlarmAssigneeComponent {
@Input()
alarm: AlarmInfo;
@Output()
alarmReassigned = new EventEmitter<boolean>();
constructor(private utilsService: UtilsService,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef) {
}
getUserDisplayName(entity: AlarmAssignee) {
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: AlarmAssignee): 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: AlarmAssignee): 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: AlarmAssignee) {
return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60);
}
openAlarmAssigneePanel($event: Event, alarm: 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: 'end',
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: alarm.id.id,
assigneeId: alarm.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.alarmReassigned.emit(true));
}
}

55
ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html

@ -0,0 +1,55 @@
<!--
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.
-->
<div style="min-width: 600px;">
<mat-toolbar color="primary">
<h2>{{ 'alarm.comments' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<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 style="padding: 0">
<tb-alarm-comment #alarmCommentComponent [alarmId]="alarmId"
[commentsHeaderEnabled]="commentsHeaderEnabled">
</tb-alarm-comment>
</div>
<div mat-dialog-actions fxLayout="row">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="close()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
<span fxFlex></span>
<button mat-raised-button color="primary"
type="button"
(click)="changeSortDirection()">
{{ 'alarm-comment.sort-direction' | translate}}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="refresh()">
{{ 'alarm-comment.refresh' | translate}}
</button>
</div>
</div>

64
ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts

@ -0,0 +1,64 @@
///
/// 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 { Component, Inject, ViewChild } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { AlarmInfo } from '@shared/models/alarm.models';
import { AlarmCommentComponent } from '@home/components/alarm/alarm-comment.component';
export interface AlarmCommentDialogData {
alarmId?: string;
alarm?: AlarmInfo;
commentsHeaderEnabled: boolean;
}
@Component({
selector: 'tb-alarm-comment-dialog',
templateUrl: './alarm-comment-dialog.component.html',
styleUrls: []
})
export class AlarmCommentDialogComponent extends DialogComponent<AlarmCommentDialogComponent, void> {
alarmId: string;
commentsHeaderEnabled: boolean = false;
@ViewChild('alarmCommentComponent', { static: true }) alarmCommentComponent: AlarmCommentComponent;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmCommentDialogData,
public dialogRef: MatDialogRef<AlarmCommentDialogComponent, void>) {
super(store, router, dialogRef);
this.commentsHeaderEnabled = this.data.commentsHeaderEnabled
this.alarmId = this.data.alarmId;
}
changeSortDirection() {
this.alarmCommentComponent.changeSortDirection();
}
refresh() {
this.alarmCommentComponent.loadAlarmComments();
}
close(): void {
this.dialogRef.close();
}
}

161
ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html

@ -0,0 +1,161 @@
<!--
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.
-->
<div fxFlexFill [formGroup]="alarmCommentFormGroup" class="tb-alarm-comments" fxLayout="column">
<div *ngIf="commentsHeaderEnabled" class="tb-alarm-comments-header" fxLayout="row" fxLayoutAlign="space-between baseline">
<span class="tb-alarm-comments-header-title">{{ 'alarm-comment.comments' | translate }}</span>
<div>
<button mat-icon-button
type="button"
(click)="changeSortDirection()"
matTooltip="{{ 'alarm-comment.sort-direction' | translate }}"
matTooltipPosition="above">
<mat-icon class="material-icons">{{ getSortDirectionIcon() }}</mat-icon>
</button>
<button mat-icon-button
type="button"
(click)="loadAlarmComments()"
matTooltip="{{ 'alarm-comment.refresh' | translate }}"
matTooltipPosition="above">
<mat-icon class="material-icons">refresh</mat-icon>
</button>
</div>
</div>
<div fxLayout="column" fxLayoutGap="32px">
<ng-container *ngIf="isDirectionDescending()">
<ng-container *ngTemplateOutlet="commentInput"></ng-container>
</ng-container>
<div fxFlex *ngFor="let displayDataElement of displayData; let i = index">
<div style="margin-left: 38px"
*ngIf="displayDataElement.isSystemComment; else userComment">
<span class="tb-alarm-comments-system-text"
style="margin-right: 8px">
{{ displayDataElement.commentText }}
</span>
<span class="tb-alarm-comments-time">
{{ displayDataElement.createdDateAgo }}
</span>
</div>
<ng-template #userComment>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"
*ngIf="!displayDataElement.edit; else commentEditing"
(mouseenter)="onCommentMouseEnter(displayDataElement.commentId, i)"
(mouseleave)="onCommentMouseLeave(i)">
<div fxLayout="row" fxLayoutAlign="center center" fxFlexAlign="start"
class="tb-alarm-comments-user-avatar"
[style.background-color]="displayDataElement.avatarBgColor">
{{ getUserInitials(displayDataElement.displayName) }}
</div>
<div fxFlex fxLayout="column" fxLayoutGap="5px">
<div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<span class="tb-alarm-comments-user-name">{{ displayDataElement.displayName }}</span>
<span class="tb-alarm-comments-time" *ngIf="displayDataElement.isEdited">
edited {{ displayDataElement.editedDateAgo }}
</span>
<span class="tb-alarm-comments-time" *ngIf="!displayDataElement.isEdited">
{{ displayDataElement.createdDateAgo }}
</span>
</div>
<span class="tb-alarm-comments-text">{{ displayDataElement.commentText }}</span>
</div>
<div fxLayout="row" class="tb-alarm-comments-action-buttons"
[ngClass]="{ 'show-buttons': displayDataElement.showActions }">
<button mat-button mat-icon-button
type="button"
(click)="editComment(displayDataElement.commentId)">
<mat-icon class="material-icons">edit</mat-icon>
</button>
<button mat-button mat-icon-button
type="button"
(click)="deleteComment(displayDataElement.commentId)">
<mat-icon class="material-icons">delete</mat-icon>
</button>
</div>
</div>
<ng-template #commentEditing>
<div fxLayoutAlign="row center" fxLayoutGap="8px">
<div fxLayout="row" fxLayoutAlign="center center"
class="tb-alarm-comments-user-avatar"
[style.background-color]="displayDataElement.avatarBgColor">
{{ getUserInitials(displayDataElement.displayName) }}
</div>
<mat-form-field fxFlex appearance="standard" class="mat-block">
<textarea matInput
type="text"
placeholder="{{ 'alarm-comment.add' | translate }}"
cdkTextareaAutosize
cdkAutosizeMinRows="1"
cols="1"
formControlName="alarmCommentEdit"
(keyup.enter)="saveEditedComment(displayDataElement.commentId)"
(keydown.enter)="$event.preventDefault()">
</textarea>
<div matSuffix fxLayout="row">
<button mat-icon-button
(click)="cancelEdit(displayDataElement.commentId)"
type="button">
<mat-icon class="material-icons red-button">close</mat-icon>
</button>
<button mat-icon-button
type="button"
(click)="saveEditedComment(displayDataElement.commentId)">
<mat-icon class="material-icons green-button">check</mat-icon>
</button>
</div>
</mat-form-field>
</div>
</ng-template>
</ng-template>
</div>
<ng-container *ngIf="isDirectionAscending()">
<ng-container *ngTemplateOutlet="commentInput"></ng-container>
</ng-container>
</div>
<ng-template #commentInput>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex>
<div fxLayout="row" fxLayoutAlign="center center"
class="tb-alarm-comments-user-avatar"
*ngIf="userDisplayName$ | async; let userDisplayName"
[style.background-color]="getCurrentUserBgColor(userDisplayName)">
{{ getUserInitials(userDisplayName) }}
</div>
<mat-form-field appearance="standard" fxFlex class="mat-block">
<textarea matInput
type="text"
placeholder="{{ 'alarm-comment.add' | translate }}"
cdkTextareaAutosize
cdkAutosizeMinRows="1"
cols="1"
formControlName="alarmComment"
(keyup.enter)="saveComment()"
(keydown.enter)="$event.preventDefault()">
</textarea>
<button mat-button
mat-icon-button
type="button"
matSuffix
*ngIf="getAlarmCommentFormControl().value"
(click)="saveComment()">
<mat-icon color="primary">
send
</mat-icon>
</button>
</mat-form-field>
</div>
</ng-template>
</div>

107
ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.scss

@ -0,0 +1,107 @@
/**
* 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 {
.tb-alarm-comments {
padding: 16px 24px 24px 24px;
background-color: #fafafa;
max-width: 600px;
&-header {
background-color: #fafafa;
position: sticky;
top: -25px;
z-index: 1;
margin-bottom: 10px;
&-title {
color: rgba(0, 0, 0, 0.76);
letter-spacing: 0.25px;
font-weight: 500;
}
.mat-icon {
color: rgba(0, 0, 0, 0.38);
}
}
&-user-avatar {
width: 28px;
min-width: 28px;
height: 28px;
min-height: 28px;
border-radius: 50%;
font-weight: 700;
color: #FFFFFF;
font-size: 13px;
}
&-user-name {
font-size: 16px;
color: rgba(0, 0, 0, 0.76);
font-weight: 500;
letter-spacing: 0.25px;
}
&-time {
font-size: 14px;
font-weight: 400;
color: rgba(0, 0, 0, 0.38);
letter-spacing: 0.2px
}
&-system-text {
color: rgba(0, 0, 0, 0.38);
font-weight: 500;
letter-spacing: 0.25px;
}
&-text {
white-space: pre-line;
word-break: break-word;
color: rgba(0, 0, 0, 0.54);
letter-spacing: 0.15px;
}
&-action-buttons {
visibility: hidden;
.mat-icon {
color: rgba(0, 0, 0, 0.38);
}
}
.show-buttons {
visibility: visible;
}
.green-button {
color: #00695C;
}
.red-button {
color: #D12730;
}
.mat-form-field {
font-size: 16px;
letter-spacing: 0.15px;
color: rgba(0, 0, 0, 0.76);
}
textarea {
letter-spacing: 0.15px;
}
}
}

295
ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts

@ -0,0 +1,295 @@
///
/// 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 { Component, Input, OnInit } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { AlarmCommentService } from '@core/http/alarm-comment.service';
import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms';
import { DialogService } from '@core/services/dialog.service';
import { AuthUser, User } from '@shared/models/user.model';
import { getCurrentAuthUser, selectUserDetails } from '@core/auth/auth.selectors';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link';
import { DateAgoPipe } from '@shared/pipe/date-ago.pipe';
import { map } from 'rxjs/operators';
import { AlarmComment, AlarmCommentInfo, AlarmCommentType } from '@shared/models/alarm.models';
import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models';
interface AlarmCommentsDisplayData {
commentId?: string,
displayName?: string,
createdDateAgo?: string,
edit?: boolean,
isEdited?: boolean,
editedDateAgo?: string,
showActions?: boolean,
commentText?: string,
isSystemComment?: boolean,
avatarBgColor?: string
}
@Component({
selector: 'tb-alarm-comment',
templateUrl: './alarm-comment.component.html',
styleUrls: ['./alarm-comment.component.scss']
})
export class AlarmCommentComponent implements OnInit {
@Input()
alarmId: string;
@Input()
commentsHeaderEnabled: boolean = true;
authUser: AuthUser;
alarmCommentFormGroup: FormGroup;
alarmComments: Array<AlarmComment>;
displayData: Array<AlarmCommentsDisplayData> = new Array<AlarmCommentsDisplayData>();
alarmCommentSortOrder: SortOrder = {
property: 'createdTime',
direction: Direction.DESC
};
editMode: boolean = false;
userDisplayName$ = this.store.pipe(
select(selectUserDetails),
map((user) => this.getUserDisplayName(user))
);
currentUserDisplayName: string;
currentUserAvatarColor: string;
constructor(protected store: Store<AppState>,
private translate: TranslateService,
private alarmCommentService: AlarmCommentService,
public fb: FormBuilder,
private dialogService: DialogService,
public dateAgoPipe: DateAgoPipe,
private utilsService: UtilsService) {
this.authUser = getCurrentAuthUser(store);
this.alarmCommentFormGroup = this.fb.group(
{
alarmCommentEdit: [''],
alarmComment: ['']
}
);
}
ngOnInit() {
this.loadAlarmComments();
this.currentUserAvatarColor = this.utilsService.stringToHslColor(this.currentUserDisplayName,
60, 40);
}
loadAlarmComments(): void {
this.alarmCommentService.getAlarmComments(this.alarmId, new PageLink(MAX_SAFE_PAGE_SIZE, 0, null,
this.alarmCommentSortOrder)).subscribe(
(pagedData) => {
this.alarmComments = pagedData.data;
this.displayData.length = 0;
for (let alarmComment of pagedData.data) {
let displayDataElement: AlarmCommentsDisplayData = {};
displayDataElement.createdDateAgo = this.dateAgoPipe.transform(alarmComment.createdTime);
displayDataElement.commentText = alarmComment.comment.text;
displayDataElement.isSystemComment = alarmComment.type === AlarmCommentType.SYSTEM;
if (alarmComment.type === AlarmCommentType.OTHER) {
displayDataElement.commentId = alarmComment.id.id;
displayDataElement.displayName = this.getUserDisplayName(alarmComment);
displayDataElement.edit = false;
displayDataElement.isEdited = alarmComment.comment.edited;
displayDataElement.editedDateAgo = this.dateAgoPipe.transform(alarmComment.comment.editedOn).toLowerCase();
displayDataElement.showActions = false;
displayDataElement.isSystemComment = false;
displayDataElement.avatarBgColor = this.utilsService.stringToHslColor(displayDataElement.displayName,
40, 60);
}
this.displayData.push(displayDataElement);
}
}
)
}
changeSortDirection() {
let currentDirection = this.alarmCommentSortOrder.direction;
this.alarmCommentSortOrder.direction = currentDirection === Direction.DESC ? Direction.ASC : Direction.DESC;
this.loadAlarmComments();
}
saveComment(): void {
const commentInputValue: string = this.getAlarmCommentFormControl().value;
if (commentInputValue) {
const comment: AlarmComment = {
alarmId: {
id: this.alarmId,
entityType: EntityType.ALARM
},
type: AlarmCommentType.OTHER,
comment: {
text: commentInputValue
}
}
this.doSave(comment);
this.clearCommentInput();
}
}
saveEditedComment(commentId: string): void {
const commentEditInputValue: string = this.getAlarmCommentEditFormControl().value;
if (commentEditInputValue) {
const editedComment: AlarmComment = this.getAlarmCommentById(commentId);
editedComment.comment.text = commentEditInputValue;
this.doSave(editedComment);
this.clearCommentEditInput();
this.editMode = false;
this.getAlarmCommentFormControl().enable({emitEvent: false});
}
}
private doSave(comment: AlarmComment): void {
this.alarmCommentService.saveAlarmComment(this.alarmId, comment).subscribe(
() => {
this.loadAlarmComments();
}
)
}
editComment(commentId: string): void {
const commentDisplayData = this.getDataElementByCommentId(commentId);
commentDisplayData.edit = true;
this.editMode = true;
this.getAlarmCommentEditFormControl().patchValue(commentDisplayData.commentText);
this.getAlarmCommentFormControl().disable({emitEvent: false});
}
cancelEdit(commentId: string): void {
const commentDisplayData = this.getDataElementByCommentId(commentId);
commentDisplayData.edit = false;
this.editMode = false;
this.getAlarmCommentFormControl().enable({emitEvent: false});
}
deleteComment(commentId: string): void {
const alarmCommentInfo: AlarmComment = this.getAlarmCommentById(commentId);
const commentText: string = alarmCommentInfo.comment.text;
this.dialogService.confirm(
this.translate.instant('alarm-comment.delete-alarm-comment'),
commentText,
this.translate.instant('action.cancel'),
this.translate.instant('action.delete')).subscribe(
(result) => {
if (result) {
this.alarmCommentService.deleteAlarmComments(this.alarmId, commentId).subscribe(
() => {
this.loadAlarmComments();
}
)
}
}
)
}
getSortDirectionIcon() {
return this.alarmCommentSortOrder.direction === Direction.DESC ? 'arrow_downward' : 'arrow_upward'
}
isDirectionAscending() {
return this.alarmCommentSortOrder.direction === Direction.ASC;
}
isDirectionDescending() {
return this.alarmCommentSortOrder.direction === Direction.DESC;
}
onCommentMouseEnter(commentId: string, displayDataIndex: number): void {
if (!this.editMode) {
const alarmUserId = this.getAlarmCommentById(commentId).userId.id;
if (this.authUser.userId === alarmUserId) {
this.displayData[displayDataIndex].showActions = true;
}
}
}
onCommentMouseLeave(displayDataIndex: number): void {
this.displayData[displayDataIndex].showActions = false;
}
getUserInitials(userName: string): string {
let initials = '';
const userNameSplit = userName.split(' ');
initials += userNameSplit[0].charAt(0).toUpperCase();
if (userNameSplit.length > 1) {
initials += userNameSplit[userNameSplit.length - 1].charAt(0).toUpperCase();
}
return initials;
}
getCurrentUserBgColor(userDisplayName: string) {
return this.utilsService.stringToHslColor(userDisplayName, 40, 60);
}
private getUserDisplayName(alarmCommentInfo: AlarmCommentInfo | User): string {
let name = '';
if ((alarmCommentInfo.firstName && alarmCommentInfo.firstName.length > 0) ||
(alarmCommentInfo.lastName && alarmCommentInfo.lastName.length > 0)) {
if (alarmCommentInfo.firstName) {
name += alarmCommentInfo.firstName;
}
if (alarmCommentInfo.lastName) {
if (name.length > 0) {
name += ' ';
}
name += alarmCommentInfo.lastName;
}
} else {
name = alarmCommentInfo.email;
}
return name;
}
getAlarmCommentFormControl(): AbstractControl {
return this.alarmCommentFormGroup.get('alarmComment');
}
getAlarmCommentEditFormControl(): AbstractControl {
return this.alarmCommentFormGroup.get('alarmCommentEdit');
}
private clearCommentInput(): void {
this.getAlarmCommentFormControl().patchValue('');
}
private clearCommentEditInput(): void {
this.getAlarmCommentEditFormControl().patchValue('');
}
private getAlarmCommentById(id: string): AlarmComment {
return this.alarmComments.find(comment => comment.id.id === id);
}
private getDataElementByCommentId(commentId: string): AlarmCommentsDisplayData {
return this.displayData.find(commentDisplayData => commentDisplayData.commentId === commentId);
}
}

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

@ -56,14 +56,10 @@
<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>
<tb-alarm-assignee fxFlex style="padding-top: 9px"
[alarm]="alarm$ | async"
(alarmReassigned)="onReassign()">
</tb-alarm-assignee>
</div>
<mat-expansion-panel class="tb-alarm-details">
<mat-expansion-panel-header fxLayout="row wrap">
@ -107,6 +103,7 @@
</ng-template>
</mat-expansion-panel>
</fieldset>
<tb-alarm-comment #alarmCommentComponent [alarmId]="alarmId"></tb-alarm-comment>
</div>
<div mat-dialog-actions fxLayout="row">
<button mat-button color="primary"

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

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, Inject, OnInit } from '@angular/core';
import { Component, Inject, OnInit, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -33,7 +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';
import { AlarmCommentComponent } from '@home/components/alarm/alarm-comment.component';
export interface AlarmDetailsDialogData {
alarmId?: string;
@ -67,7 +67,7 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
alarmUpdated = false;
assigneeInitials = '';
@ViewChild('alarmCommentComponent', { static: true }) alarmCommentComponent: AlarmCommentComponent;
constructor(protected store: Store<AppState>,
protected router: Router,
@ -76,8 +76,7 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
@Inject(MAT_DIALOG_DATA) public data: AlarmDetailsDialogData,
private alarmService: AlarmService,
public dialogRef: MatDialogRef<AlarmDetailsDialogComponent, boolean>,
public fb: UntypedFormBuilder,
private utilsService: UtilsService) {
public fb: UntypedFormBuilder) {
super(store, router, dialogRef);
this.allowAcknowledgment = data.allowAcknowledgment;
@ -88,15 +87,12 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
{
createdTime: [''],
originatorName: [''],
assigneeId: [''],
startTime: [''],
endTime: [''],
ackTime: [''],
clearTime: [''],
assignTime: [''],
type: [''],
alarmSeverity: [''],
assignee: [''],
alarmStatus: [''],
alarmDetails: [null]
}
@ -122,10 +118,6 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
.patchValue(this.datePipe.transform(alarm.createdTime, 'yyyy-MM-dd HH:mm:ss'));
this.alarmFormGroup.get('originatorName')
.patchValue(alarm.originatorName);
if(alarm.assigneeId) {
this.alarmFormGroup.get('assigneeId')
.patchValue(alarm.assigneeId.id);
}
if (alarm.startTs) {
this.alarmFormGroup.get('startTime')
.patchValue(this.datePipe.transform(alarm.startTs, 'yyyy-MM-dd HH:mm:ss'));
@ -142,20 +134,11 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
this.alarmFormGroup.get('clearTime')
.patchValue(this.datePipe.transform(alarm.clearTs, 'yyyy-MM-dd HH:mm:ss'));
}
if (alarm.assignTs) {
this.alarmFormGroup.get('assignTime')
.patchValue(this.datePipe.transform(alarm.assignTs, 'yyyy-MM-dd HH:mm:ss'));
}
this.alarmFormGroup.get('type').patchValue(alarm.type);
this.alarmFormGroup.get('alarmSeverity')
.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);
}
@ -172,6 +155,7 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
() => {
this.alarmUpdated = true;
this.loadAlarm();
this.alarmCommentComponent.loadAlarmComments();
}
);
}
@ -183,48 +167,15 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
() => {
this.alarmUpdated = true;
this.loadAlarm();
this.alarmCommentComponent.loadAlarmComments();
}
);
}
}
getColorFromString(userDisplayName: string) {
return this.utilsService.stringToHslColor(userDisplayName, 40, 60);
onReassign(): void {
this.alarmUpdated = true;
this.loadAlarm()
this.alarmCommentComponent.loadAlarmComments();
}
getUserDisplayName(entity: AlarmInfo) {
let displayName = '';
if ((entity.assignee.firstName && entity.assignee.firstName.length > 0) ||
(entity.assignee.lastName && entity.assignee.lastName.length > 0)) {
if (entity.assignee.firstName) {
displayName += entity.assignee.firstName;
}
if (entity.assignee.lastName) {
if (displayName.length > 0) {
displayName += ' ';
}
displayName += entity.assignee.lastName;
}
} else {
displayName = entity.assignee.email;
}
return displayName;
}
getUserInitials(entity: AlarmInfo): string {
let initials = '';
if (entity.assignee.firstName && entity.assignee.firstName.length ||
entity.assignee.lastName && entity.assignee.lastName.length) {
if (entity.assignee.firstName) {
initials += entity.assignee.firstName.charAt(0);
}
if (entity.assignee.lastName) {
initials += entity.assignee.lastName.charAt(0);
}
} else {
initials += entity.assignee.email.charAt(0);
}
return initials.toUpperCase();
}
}

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

@ -249,7 +249,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'center',
overlayX: 'end',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)

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

@ -19,6 +19,9 @@ import { CommonModule } from '@angular/common';
import { SharedModule } from '@app/shared/shared.module';
import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-details-dialog.component';
import { SHARED_HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
import { AlarmCommentComponent } from '@home/components/alarm/alarm-comment.component';
import { AlarmCommentDialogComponent } from '@home/components/alarm/alarm-comment-dialog.component';
import { AlarmAssigneeComponent } from '@home/components/alarm/alarm-assignee.component';
@NgModule({
providers: [
@ -26,14 +29,20 @@ import { SHARED_HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
],
declarations:
[
AlarmDetailsDialogComponent
AlarmDetailsDialogComponent,
AlarmCommentComponent,
AlarmCommentDialogComponent,
AlarmAssigneeComponent
],
imports: [
CommonModule,
SharedModule
],
exports: [
AlarmDetailsDialogComponent
AlarmDetailsDialogComponent,
AlarmCommentComponent,
AlarmCommentDialogComponent,
AlarmAssigneeComponent
]
})
export class SharedHomeComponentsModule { }

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

@ -131,6 +131,10 @@ import {
ALARM_ASSIGNEE_PANEL_DATA, AlarmAssigneePanelComponent,
AlarmAssigneePanelData
} from '@home/components/alarm/alarm-assignee-panel.component';
import {
AlarmCommentDialogComponent,
AlarmCommentDialogData
} from '@home/components/alarm/alarm-comment-dialog.component';
interface AlarmsTableWidgetSettings extends TableWidgetSettings {
alarmsTitle: string;
@ -143,12 +147,14 @@ interface AlarmsTableWidgetSettings extends TableWidgetSettings {
allowAcknowledgment: boolean;
allowClear: boolean;
allowAssign: boolean;
displayComments: boolean;
}
interface AlarmWidgetActionDescriptor extends TableCellButtonActionDescriptor {
details?: boolean;
acknowledge?: boolean;
clear?: boolean;
comments?: boolean;
}
@Component({
@ -195,6 +201,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
public allowAcknowledgment = true;
private allowClear = true;
public allowAssign = true;
private displayComments = false;
private defaultPageSize = 10;
private defaultSortOrder = '-' + alarmFields.createdTime.value;
@ -331,6 +338,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.displayDetails = isDefined(this.settings.displayDetails) ? this.settings.displayDetails : true;
this.allowAcknowledgment = isDefined(this.settings.allowAcknowledgment) ? this.settings.allowAcknowledgment : true;
this.allowClear = isDefined(this.settings.allowClear) ? this.settings.allowClear : true;
this.displayComments = isDefined(this.settings.displayComments) ? this.settings.displayComments : false;
this.allowAssign = isDefined(this.settings.allowAssign) ? this.settings.allowAssign : true;
if (this.settings.alarmsTitle && this.settings.alarmsTitle.length) {
@ -486,6 +494,16 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
);
}
if (this.displayComments) {
actionCellDescriptors.push(
{
displayName: this.translate.instant('alarm-comment.comments'),
icon: 'comment',
comments: true
} as AlarmWidgetActionDescriptor
);
}
this.setCellButtonAction = !!(actionCellDescriptors.length + this.ctx.actionsApi.getActionDescriptors('actionCellButton').length);
if (this.setCellButtonAction) {
@ -801,6 +819,8 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.ackAlarm($event, alarm);
} else if (actionDescriptor.clear) {
this.clearAlarm($event, alarm);
} else if (actionDescriptor.comments) {
this.openAlarmComments($event, alarm);
} else {
if ($event) {
$event.stopPropagation();
@ -964,6 +984,24 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
}
}
private openAlarmComments($event: Event, alarm: AlarmDataInfo) {
if ($event) {
$event.stopPropagation();
}
if (alarm && alarm.id && alarm.id.id !== NULL_UUID) {
this.dialog.open<AlarmCommentDialogComponent, AlarmCommentDialogData, void>
(AlarmCommentDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
alarmId: alarm.id.id,
commentsHeaderEnabled: false
}
}).afterClosed()
}
}
private defaultContent(key: EntityColumn, contentInfo: CellContentInfo, value: any): any {
if (isDefined(value)) {
const alarmField = alarmFields[key.name];
@ -1077,7 +1115,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'center',
overlayX: 'end',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)

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

@ -70,6 +70,9 @@
<mat-slide-toggle formControlName="allowAssign">
{{ 'widgets.table.allow-alarms-assign' | translate }}
</mat-slide-toggle>
<mat-slide-toggle formControlName="displayComments">
{{ 'widgets.table.display-alarm-comments' | translate }}
</mat-slide-toggle>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="8px" fxLayoutAlign.gt-xs="start center">
<mat-slide-toggle fxFlex formControlName="displayPagination">
{{ 'widgets.table.display-pagination' | translate }}

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

@ -74,6 +74,7 @@ export class AlarmsTableWidgetSettingsComponent extends WidgetSettingsComponent
allowAcknowledgment: [settings.allowAcknowledgment, []],
allowClear: [settings.allowClear, []],
allowAssign: [settings.allowAssign, []],
displayComments: [settings.displayComments, []],
displayPagination: [settings.displayPagination, []],
defaultPageSize: [settings.defaultPageSize, [Validators.min(1)]],
defaultSortOrder: [settings.defaultSortOrder, []],

25
ui-ngx/src/app/shared/models/alarm.models.ts

@ -23,7 +23,8 @@ import { NULL_UUID } from '@shared/models/id/has-uuid';
import { EntityType } from '@shared/models/entity-type.models';
import { CustomerId } from '@shared/models/id/customer-id';
import { TableCellButtonActionDescriptor } from '@home/components/widget/lib/table-widget.models';
import { UserId } from "@shared/models/id/user-id";
import { AlarmCommentId } from '@shared/models/id/alarm-comment-id';
import { UserId } from '@shared/models/id/user-id';
export enum AlarmSeverity {
CRITICAL = 'CRITICAL',
@ -104,6 +105,28 @@ export interface Alarm extends BaseData<AlarmId> {
details?: any;
}
export enum AlarmCommentType {
SYSTEM = 'SYSTEM',
OTHER = 'OTHER'
}
export interface AlarmComment extends BaseData<AlarmCommentId> {
alarmId: AlarmId;
userId?: UserId;
type: AlarmCommentType;
comment: {
text: string;
edited?: boolean;
editedOn?: number;
}
}
export interface AlarmCommentInfo extends AlarmComment {
firstName?: string;
lastName?: string;
email?: string;
}
export interface AlarmInfo extends Alarm {
originatorName: string;
originatorLabel: string;

24
ui-ngx/src/app/shared/models/id/alarm-comment-id.ts

@ -0,0 +1,24 @@
///
/// 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 { HasUUID } from '@shared/models/id/has-uuid';
export class AlarmCommentId implements HasUUID {
id: string;
constructor(id: string) {
this.id = id;
}
}

58
ui-ngx/src/app/shared/pipe/date-ago.pipe.ts

@ -0,0 +1,58 @@
///
/// 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 { Inject, Pipe, PipeTransform } from '@angular/core';
import { DAY, HOUR, MINUTE, SECOND, WEEK, YEAR } from '@shared/models/time/time.models';
import { TranslateService } from '@ngx-translate/core';
const intervals = {
years: YEAR,
months: DAY * 30,
weeks: WEEK,
days: DAY,
hr: HOUR,
min: MINUTE,
sec: SECOND
};
@Pipe({
name: 'dateAgo'
})
export class DateAgoPipe implements PipeTransform {
constructor(@Inject(TranslateService) private translate: TranslateService) {
}
transform(value: number): string {
if (value) {
const ms = Math.floor((+new Date() - +new Date(value)));
if (ms < 29 * SECOND) { // less than 30 seconds ago will show as 'Just now'
return this.translate.instant('timewindow.just-now');
}
let counter;
// tslint:disable-next-line:forin
for (const i in intervals) {
counter = Math.floor(ms / intervals[i]);
if (counter > 0) {
return this.translate.instant(`timewindow.${i}`, {[i]: counter});
}
}
}
return '';
}
}

8
ui-ngx/src/app/shared/shared.module.ts

@ -169,6 +169,7 @@ import { PhoneInputComponent } from '@shared/components/phone-input.component';
import { CustomDateAdapter } from '@shared/adapter/custom-datatime-adapter';
import { CustomPaginatorIntl } from '@shared/services/custom-paginator-intl';
import { TbScriptLangComponent } from '@shared/components/script-lang.component';
import { DateAgoPipe } from '@shared/pipe/date-ago.pipe';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -184,6 +185,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
TbJsonPipe,
FileSizePipe,
SafePipe,
DateAgoPipe,
{
provide: FlowInjectionToken,
useValue: Flow
@ -295,7 +297,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ProtobufContentComponent,
BranchAutocompleteComponent,
PhoneInputComponent,
TbScriptLangComponent
TbScriptLangComponent,
DateAgoPipe
],
imports: [
CommonModule,
@ -500,7 +503,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ProtobufContentComponent,
BranchAutocompleteComponent,
PhoneInputComponent,
TbScriptLangComponent
TbScriptLangComponent,
DateAgoPipe
]
})
export class SharedModule { }

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

@ -451,7 +451,6 @@
"end-time": "End time",
"ack-time": "Acknowledged time",
"clear-time": "Cleared time",
"assign-time": "Assign time",
"alarm-severity-list": "Alarm severity list",
"any-severity": "Any severity",
"severity-critical": "Critical",
@ -486,8 +485,17 @@
"alarm-type-list": "Alarm type list",
"any-type": "Any type",
"search-propagated-alarms": "Search propagated alarms",
"comments": "Alarm comments",
"advanced-info": "Advanced info"
},
"alarm-comment": {
"add": "Add a comment...",
"alarm-comment": "Alarm comment",
"comments": "Comments",
"delete-alarm-comment": "Do you want to delete this comment?",
"refresh": "Refresh",
"sort-direction": "Sort direction"
},
"alias": {
"add": "Add alias",
"edit": "Edit alias",
@ -3397,16 +3405,16 @@
"days": "Days"
},
"timewindow": {
"years": "{ years, plural, 1 { year } other {# years } }",
"months": "{ months, plural, 1 { month } other {# months } }",
"weeks": "{ weeks, plural, 1 { week } other {# weeks } }",
"days": "{ days, plural, 1 { day } other {# days } }",
"hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }",
"hr": "{{ hr }} hr",
"minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }",
"min": "{{ min }} min",
"seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }",
"short": {
"days": "{ days, plural, 1 {1 day } other {# days } }",
"hours": "{ hours, plural, 1 {1 hour } other {# hours } }",
"minutes": "{{minutes}} min ",
"seconds": "{{seconds}} sec "
},
"sec": "{{ sec }} sec",
"realtime": "Realtime",
"history": "History",
"last-prefix": "last",
@ -3416,7 +3424,8 @@
"last": "Last",
"time-period": "Time period",
"hide": "Hide",
"interval": "Interval"
"interval": "Interval",
"just-now": "Just now"
},
"user": {
"user": "User",
@ -4748,6 +4757,7 @@
"display-alarm-details": "Display alarm details",
"allow-alarms-ack": "Allow alarms acknowledgment",
"allow-alarms-clear": "Allow alarms clear",
"display-alarm-comments": "Display alarm comments",
"allow-alarms-assign": "Allow alarms assignment"
},
"value-source": {

Loading…
Cancel
Save