Merge branch 'alarmCommentsUI' of github.com:rusikv/thingsboard into alarmCommentsUI

This commit is contained in:
rusikv 2023-02-24 16:23:16 +02:00
commit d5d2b043a2
17 changed files with 915 additions and 14 deletions

View File

@ -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));
}
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -107,6 +107,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"

View File

@ -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,6 +33,7 @@ import { AlarmService } from '@core/http/alarm.service';
import { tap } from 'rxjs/operators';
import { DatePipe } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { AlarmCommentComponent } from '@home/components/alarm/alarm-comment.component';
import { UtilsService } from '@core/services/utils.service';
export interface AlarmDetailsDialogData {
@ -67,6 +68,8 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
alarmUpdated = false;
@ViewChild('alarmCommentComponent', { static: true }) alarmCommentComponent: AlarmCommentComponent;
assigneeInitials = '';
constructor(protected store: Store<AppState>,
@ -172,6 +175,7 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
() => {
this.alarmUpdated = true;
this.loadAlarm();
this.alarmCommentComponent.loadAlarmComments();
}
);
}
@ -183,6 +187,7 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
() => {
this.alarmUpdated = true;
this.loadAlarm();
this.alarmCommentComponent.loadAlarmComments();
}
);
}

View File

@ -19,6 +19,8 @@ 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';
@NgModule({
providers: [
@ -26,14 +28,18 @@ import { SHARED_HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
],
declarations:
[
AlarmDetailsDialogComponent
AlarmDetailsDialogComponent,
AlarmCommentComponent,
AlarmCommentDialogComponent
],
imports: [
CommonModule,
SharedModule
],
exports: [
AlarmDetailsDialogComponent
AlarmDetailsDialogComponent,
AlarmCommentComponent,
AlarmCommentDialogComponent
]
})
export class SharedHomeComponentsModule { }

View File

@ -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];

View File

@ -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 }}

View File

@ -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, []],

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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 '';
}
}

View File

@ -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 { }

View File

@ -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": {