diff --git a/application/src/main/data/json/demo/dashboards/thermostats.json b/application/src/main/data/json/demo/dashboards/thermostats.json index 4f486effdf..c014d8875a 100644 --- a/application/src/main/data/json/demo/dashboards/thermostats.json +++ b/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 } ] }, diff --git a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json index 448fcaf52c..e7a97b36f8 100644 --- a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json +++ b/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}" } } ] diff --git a/ui-ngx/src/app/core/http/alarm-comment.service.ts b/ui-ngx/src/app/core/http/alarm-comment.service.ts new file mode 100644 index 0000000000..ffb8fc4528 --- /dev/null +++ b/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 { + return this.http.post(`/api/alarm/${alarmId}/comment`, alarmComment, defaultHttpOptionsFromConfig(config)); + } + + public getAlarmComments(alarmId: string, pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/${alarmId}/comment${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public deleteAlarmComments(alarmId: string, commentId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/alarm/${alarmId}/comment/${commentId}`, defaultHttpOptionsFromConfig(config)); + } + +} 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 index 2213be1835..9829e8c436 100644 --- 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 @@ -27,12 +27,12 @@ [displayWith]="displayUserFn" (optionSelected)="selected($event)" panelWidth="260px"> - + account_circle - alarm.unassigned + alarm.unassigned - + 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 index c76aedbb18..8db3bb111f 100644 --- 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 @@ -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; } 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 index 32d6c2a1da..2cf382afcf 100644 --- 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 @@ -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); diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html new file mode 100644 index 0000000000..6e50082e8b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html @@ -0,0 +1,41 @@ + + +
+ + + {{ getUserInitials(alarm.assignee) }} + + {{ getUserDisplayName(alarm.assignee) }} + + + account_circle + alarm.unassigned + + +
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss new file mode 100644 index 0000000000..5736aa8ef1 --- /dev/null +++ b/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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts new file mode 100644 index 0000000000..5d7f29d55b --- /dev/null +++ b/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(); + + 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)); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html new file mode 100644 index 0000000000..76fac3aaae --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html @@ -0,0 +1,55 @@ + +
+ +

{{ 'alarm.comments' | translate }}

+ + +
+ + +
+
+ + +
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts new file mode 100644 index 0000000000..5f28ce905e --- /dev/null +++ b/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 { + + alarmId: string; + commentsHeaderEnabled: boolean = false; + + @ViewChild('alarmCommentComponent', { static: true }) alarmCommentComponent: AlarmCommentComponent; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmCommentDialogData, + public dialogRef: MatDialogRef) { + 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(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html new file mode 100644 index 0000000000..26f5bfdd1d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html @@ -0,0 +1,161 @@ + + +
+
+ {{ 'alarm-comment.comments' | translate }} +
+ + +
+
+
+ + + +
+
+ + {{ displayDataElement.commentText }} + + + {{ displayDataElement.createdDateAgo }} + +
+ +
+
+ {{ getUserInitials(displayDataElement.displayName) }} +
+
+
+ {{ displayDataElement.displayName }} + + edited {{ displayDataElement.editedDateAgo }} + + + {{ displayDataElement.createdDateAgo }} + +
+ {{ displayDataElement.commentText }} +
+
+ + +
+
+ +
+
+ {{ getUserInitials(displayDataElement.displayName) }} +
+ + +
+ + +
+
+
+
+
+
+ + + +
+ +
+
+ {{ getUserInitials(userDisplayName) }} +
+ + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.scss new file mode 100644 index 0000000000..3cd97767a2 --- /dev/null +++ b/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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts new file mode 100644 index 0000000000..ef6e782b1e --- /dev/null +++ b/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; + + displayData: Array = new Array(); + + 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, + 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); + } + +} 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 eb67fe32a6..c1be93431d 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 @@ -56,14 +56,10 @@ alarm.status - - alarm.assignee - - {{ assigneeInitials }} - - - + + @@ -107,6 +103,7 @@ +