Merge pull request #8285 from rusikv/alarm-details-redesign

Alarm details redesign
This commit is contained in:
Andrew Shvayka 2023-04-07 13:05:03 +03:00 committed by GitHub
commit a8d0d871e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 574 additions and 399 deletions

View File

@ -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,\"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}"
"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,\"displayActivity\":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}"
}
}
]

View File

@ -25,8 +25,7 @@
<mat-autocomplete class="tb-assignee-autocomplete"
#userAutocomplete="matAutocomplete"
[displayWith]="displayUserFn"
(optionSelected)="selected($event)"
panelWidth="260px">
(optionSelected)="selected($event)">
<mat-option [fxHide]="!assigneeId" [value]="null">
<mat-icon class="unassigned-icon">account_circle</mat-icon>
<span translate>alarm.unassigned</span>

View File

@ -17,7 +17,7 @@
:host {
width: 100%;
overflow: auto;
background: #fff;
background: white;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3), 0px 2px 6px 2px rgba(0, 0, 0, 0.15);
border-radius: 4px;
}
@ -27,7 +27,7 @@
padding: 8px;
height: 340px;
font-size: 14px;
background-color: #fff;
background-color: white;
}
.mat-form-field-appearance-outline .mdc-notched-outline__trailing{
@ -37,7 +37,6 @@
.tb-assignee-autocomplete {
&.tb-assignee-autocomplete.mat-mdc-autocomplete-panel {
position: relative;
left: -8px;
margin-top: 8px;
box-shadow: none !important;
}
@ -58,17 +57,16 @@
align-items: center;
margin-right: 8px;
border-radius: 50%;
background-color: #5cb445;
width: 28px;
height: 28px;
min-width: 28px;
min-height: 28px;
color: #fff;
color: white;
font-size: 13px;
font-weight: 700
}
.user-display-name {
max-width: 180px;
max-width: 80%;
overflow: hidden;
span {
overflow: hidden;
@ -79,8 +77,8 @@
}
}
.mdc-list-item__primary-text {
width: 100%;
display: flex;
justify-content: start;
align-items: center;
line-height: normal;
}

View File

@ -65,6 +65,8 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
assigneeId?: string;
reassigned: boolean = false;
selectUserFormGroup: FormGroup;
@ViewChild('userInput', {static: true}) userInput: ElementRef;
@ -130,12 +132,18 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
assign(user: User): void {
this.alarmService.assignAlarm(this.alarmId, user.id.id, {ignoreLoading: true}).subscribe(
() => this.overlayRef.dispose());
() => {
this.reassigned = true;
this.overlayRef.dispose()
});
}
unassign(): void {
this.alarmService.unassignAlarm(this.alarmId, {ignoreLoading: true}).subscribe(
() => this.overlayRef.dispose());
() => {
this.reassigned = true;
this.overlayRef.dispose()
});
}
fetchUsers(searchText?: string): Observable<Array<UserEmailInfo>> {

View File

@ -16,28 +16,15 @@
-->
<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>
<mat-form-field fxFlex class="mat-block" style="margin-bottom: 25px"
(click)="openAlarmAssigneePanel($event, alarm)"
subscriptSizing="dynamic">
<mat-label translate>alarm.assignee</mat-label>
<input matInput readonly [value]="getAssignee()">
<span *ngIf="alarm?.assigneeId" matPrefix class="user-avatar"
[style.backgroundColor]="getAvatarBgColor(alarm.assignee)">
{{ getUserInitials(alarm.assignee) }}
</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>
<mat-icon *ngIf="!alarm?.assigneeId" matPrefix class="unassigned-icon">account_circle</mat-icon>
<mat-icon matSuffix>arrow_drop_down</mat-icon>
</mat-form-field>

View File

@ -14,36 +14,29 @@
* limitations under the License.
*/
:host {
.tb-assignee {
cursor: pointer;
max-width: 273px;
.assigned-container {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.user-avatar {
display: inline-flex;
justify-content: center;
align-items: center;
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;
color: rgba(0, 0, 0, 0.38);
overflow: visible;
}
}
.user-avatar {
display: inline-flex;
justify-content: center;
align-items: center;
border-radius: 50%;
width: 28px;
height: 28px;
min-width: 28px;
min-height: 28px;
color: white;
font-size: 13px;
font-weight: 700;
margin-left: 12px;
margin-right: 20px;
}
.unassigned-icon {
width: 28px;
height: 28px;
font-size: 28px;
color: rgba(0, 0, 0, 0.38);
overflow: visible;
margin-left: 12px;
margin-right: 20px;
padding: 0;
}

View File

@ -14,17 +14,17 @@
/// limitations under the License.
///
import {
Component, EventEmitter, Injector, Input, Output, StaticProvider, ViewContainerRef
} from '@angular/core';
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,
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';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'tb-alarm-assignee',
@ -35,12 +35,26 @@ export class AlarmAssigneeComponent {
@Input()
alarm: AlarmInfo;
@Input()
allowAssign: boolean;
@Output()
alarmReassigned = new EventEmitter<boolean>();
constructor(private utilsService: UtilsService,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef) {
private viewContainerRef: ViewContainerRef,
private translateService: TranslateService) {
}
getAssignee() {
if (this.alarm) {
if (this.alarm.assignee) {
return this.getUserDisplayName(this.alarm.assignee);
} else {
return this.translateService.instant('alarm.unassigned');
}
}
}
getUserDisplayName(entity: AlarmAssignee) {
@ -103,39 +117,41 @@ export class AlarmAssigneeComponent {
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));
if (this.allowAssign) {
const target = $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'center',
originY: 'bottom',
overlayX: 'center',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
config.width = (target as HTMLElement).offsetWidth;
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));
}
}
}

View File

@ -17,11 +17,8 @@
-->
<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">
<button mat-icon-button (click)="close()" type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
@ -29,16 +26,8 @@
</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 [alarmId]="alarmId"
[alarmActivityOnly]="true">
</tb-alarm-comment>
</div>
<div mat-dialog-actions fxLayout="row" fxLayoutAlign="end">
<button mat-raised-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="close()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
</div>

View File

@ -25,7 +25,6 @@ import { AlarmInfo } from '@shared/models/alarm.models';
export interface AlarmCommentDialogData {
alarmId?: string;
alarm?: AlarmInfo;
commentsHeaderEnabled: boolean;
}
@Component({
@ -37,14 +36,11 @@ export class AlarmCommentDialogComponent extends DialogComponent<AlarmCommentDia
alarmId: string;
commentsHeaderEnabled: boolean = false;
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;
}

View File

@ -17,129 +17,147 @@
-->
<div [formGroup]="alarmCommentFormGroup" class="tb-alarm-comments" fxLayout="column">
<div class="tb-alarm-comments-header" fxLayout="row" fxLayoutAlign="space-between baseline">
<span *ngIf="commentsHeaderEnabled" class="tb-alarm-comments-header-title">
{{ 'alarm-comment.comments' | translate }}
</span>
<div style="margin-left: auto">
<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 class="header" [ngClass]="{'activity-only': alarmActivityOnly}">
<div class="header-container" fxLayout="row" fxLayoutAlign="space-between center"
[ngClass]="{'asc': isDirectionAscending(), 'activity-only': alarmActivityOnly}">
<span class="header-title" translate>
alarm-activity.activity
</span>
<div style="margin-left: auto">
<button mat-icon-button
type="button"
(click)="exportAlarmActivity()"
matTooltip="{{ 'alarm-activity.export' | translate }}"
matTooltipPosition="above">
<mat-icon class="material-icons" svgIcon="mdi:file-export"></mat-icon>
</button>
<button mat-icon-button
type="button"
(click)="changeSortDirection()"
[matTooltip]="getSortDirectionTooltipText()"
matTooltipPosition="above">
<mat-icon class="material-icons" [svgIcon]="getSortDirectionIcon()"></mat-icon>
</button>
<button mat-icon-button
type="button"
(click)="loadAlarmComments()"
matTooltip="{{ 'alarm-activity.refresh' | translate }}"
matTooltipPosition="above">
<mat-icon class="material-icons">refresh</mat-icon>
</button>
</div>
</div>
</div>
<div fxLayout="column" fxLayoutGap="32px">
<ng-container *ngIf="isDirectionDescending()">
<div fxLayout="column">
<ng-container *ngIf="!isDirectionAscending()">
<ng-container *ngTemplateOutlet="commentInput"></ng-container>
</ng-container>
<div fxFlex *ngFor="let displayDataElement of displayData; index as i">
<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-icon-button
type="button"
(click)="editComment(displayDataElement.commentId)">
<mat-icon class="material-icons">edit</mat-icon>
</button>
<button mat-icon-button
type="button"
(click)="deleteComment(displayDataElement.commentId)">
<mat-icon class="material-icons">delete</mat-icon>
</button>
</div>
<div class="comments-container" fxLayout="column" fxLayoutGap="12px" style="padding: 24px"
[ngClass]="{'asc': isDirectionAscending(), 'activity-only': alarmActivityOnly}">
<div fxFlex *ngFor="let displayDataElement of displayData; index as i">
<div class="system-comment" *ngIf="displayDataElement.isSystemComment; else userComment">
<span class="system-text" style="margin-right: 8px">
{{ displayDataElement.commentText }}
</span>
<span class="time" style="padding: 3px"
[matTooltip]="displayDataElement.createdTime"
matTooltipPosition="right">
{{ displayDataElement.createdDateAgo }}
</span>
</div>
<ng-template #commentEditing>
<div fxLayoutAlign="row center" fxLayoutGap="8px">
<div fxLayout="row" fxLayoutAlign="center center"
class="tb-alarm-comments-user-avatar"
<ng-template #userComment>
<div class="user-comment" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"
*ngIf="!displayDataElement.edit; else commentEditing"
(mouseenter)="onCommentMouseEnter(displayDataElement.commentId, i)"
(mouseleave)="onCommentMouseLeave(i)">
<div class="user-avatar" fxLayout="row" fxLayoutAlign="center center" fxFlexAlign="start" fxHide.xs
[style.background-color]="displayDataElement.avatarBgColor">
{{ getUserInitials(displayDataElement.displayName) }}
</div>
<mat-form-field fxFlex appearance="fill" 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 fxFlex fxLayout="column" fxLayoutGap="5px">
<div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<span class="user-name">{{ displayDataElement.displayName }}</span>
<span class="time"
[matTooltip]="displayDataElement.createdTime"
matTooltipPosition="right">
{{ displayDataElement.createdDateAgo }}
</span>
<span class="time" *ngIf="displayDataElement.isEdited"
matTooltip="{{ displayDataElement.editedDateAgo }} {{ displayDataElement.editedTime }}"
matTooltipPosition="right">
Edited
</span>
</div>
</mat-form-field>
<span class="text">{{ displayDataElement.commentText }}</span>
</div>
<div fxLayout="row" fxLayout.xs="column" class="action-buttons"
[ngClass]="{ 'show-buttons': displayDataElement.showActions }">
<button mat-icon-button
type="button"
(click)="editComment(displayDataElement.commentId)">
<mat-icon class="material-icons">edit</mat-icon>
</button>
<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="user-avatar"
[style.background-color]="displayDataElement.avatarBgColor">
{{ getUserInitials(displayDataElement.displayName) }}
</div>
<mat-form-field fxFlex class="mat-block tb-appearance-transparent">
<textarea matInput
type="text"
placeholder="{{ 'alarm-activity.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>
</ng-template>
</div>
</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="fill" fxFlex class="mat-block">
<div style="background-color: white" class="comment-input"
[ngClass]="{'newest-first': !isDirectionAscending(), 'oldest-first': isDirectionAscending(), 'activity-only': alarmActivityOnly}">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" class="inner-wrap">
<div fxLayout="row" fxLayoutAlign="center center"
class="user-avatar"
*ngIf="userDisplayName$ | async; let userDisplayName"
[style.background-color]="getCurrentUserBgColor(userDisplayName)">
{{ getUserInitials(userDisplayName) }}
</div>
<mat-form-field fxFlex class="mat-block tb-appearance-transparent">
<textarea matInput
type="text"
placeholder="{{ 'alarm-comment.add' | translate }}"
placeholder="{{ 'alarm-activity.add' | translate }}"
cdkTextareaAutosize
cdkAutosizeMinRows="1"
cols="1"
@ -147,16 +165,17 @@
(keyup.enter)="saveComment()"
(keydown.enter)="$event.preventDefault()">
</textarea>
<button mat-icon-button
type="button"
matSuffix
*ngIf="getAlarmCommentFormControl().value"
(click)="saveComment()">
<mat-icon color="primary">
send
</mat-icon>
</button>
</mat-form-field>
<button mat-icon-button
type="button"
matSuffix
*ngIf="getAlarmCommentFormControl().value"
(click)="saveComment()">
<mat-icon color="primary">
send
</mat-icon>
</button>
</mat-form-field>
</div>
</div>
</ng-template>
</div>

View File

@ -13,69 +13,182 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@use '@angular/material' as mat;
@import '../theme.scss';
$primary-color: rgba(mat.get-color-from-palette($tb-primary, 50), 0.4);
$border: 1px solid mat.get-color-from-palette($tb-primary);
:host {
.tb-alarm-comments {
padding: 16px 24px 24px 24px;
background-color: #fafafa;
background-color: $primary-color;
max-width: 600px;
width: 100%;
&-header {
background-color: #fafafa;
.comment-input {
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;
.inner-wrap {
border: $border;
padding: 0 24px;
background-color: $primary-color;
}
.mat-icon {
color: rgba(0, 0, 0, 0.38);
&.oldest-first {
bottom: -24px;
box-shadow: 0px -4px 12px rgba(0, 0, 0, 0.04);
.inner-wrap {
border-radius: 0 0 8px 8px;
}
&.activity-only {
bottom: -1px;
.inner-wrap {
border: none
}
}
}
&.newest-first {
top: 24px;
box-shadow: 0 8px 10px rgba(23, 33, 90, 0.08);
.inner-wrap {
border-top: none;
}
&.activity-only {
top: 48px;
box-shadow: 0 8px 10px rgba(23, 33, 90, 0.08);
.inner-wrap {
border: none;
}
}
}
}
&-user-avatar {
.header {
z-index: 1;
position: sticky;
top: -24px;
background-color: white;
&.activity-only {
top: 0;
}
&-container {
padding: 0 24px;
background-color: $primary-color;
border: $border;
border-bottom: none;
border-radius: 8px 8px 0px 0px;
&.activity-only {
border: none;
}
&.asc {
border-bottom: $border;
box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);
&.activity-only {
border: none;
}
}
.header-title {
color: rgba(0, 0, 0, 0.76);
letter-spacing: 0.25px;
font-weight: 500;
}
.mat-icon {
color: rgba(0, 0, 0, 0.38);
}
}
}
.comments-container {
border: $border;
border-top: none;
border-radius: 0px 0px 8px 8px;
&.activity-only {
border: none;
}
&.asc {
border-bottom: none;
border-radius: unset;
}
}
.user-comment {
padding: 4px;
border-radius: 8px;
&:hover {
background-color: $primary-color;
}
}
.user-avatar {
width: 28px;
min-width: 28px;
height: 28px;
min-height: 28px;
border-radius: 50%;
font-weight: 700;
color: #FFFFFF;
color: white;
font-size: 13px;
}
&-user-name {
.user-name {
font-size: 16px;
color: rgba(0, 0, 0, 0.76);
font-weight: 500;
letter-spacing: 0.25px;
}
&-time {
.time {
font-size: 14px;
font-weight: 400;
color: rgba(0, 0, 0, 0.38);
letter-spacing: 0.2px
letter-spacing: 0.2px;
border-radius: 16px;
padding: 0 3px;
&:hover {
background: rgba(0, 0, 0, 0.04);
}
}
&-system-text {
color: rgba(0, 0, 0, 0.38);
.system-comment {
margin-left: 38px;
@media #{$mat-xs} {
margin-left: 0;
}
}
.system-text {
font-weight: 500;
font-size: 14px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.38);
}
&-text {
.text {
white-space: pre-line;
word-break: break-word;
color: rgba(0, 0, 0, 0.54);
font-size: 16px;
letter-spacing: 0.15px;
color: rgba(0, 0, 0, 0.54);
}
&-action-buttons {
.action-buttons {
visibility: hidden;
.mat-icon {
color: rgba(0, 0, 0, 0.38);

View File

@ -30,13 +30,17 @@ 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';
import { DatePipe } from '@angular/common';
import { ImportExportService } from '@home/components/import-export/import-export.service';
interface AlarmCommentsDisplayData {
commentId?: string,
displayName?: string,
createdTime: string,
createdDateAgo?: string,
edit?: boolean,
isEdited?: boolean,
editedTime?: string;
editedDateAgo?: string,
showActions?: boolean,
commentText?: string,
@ -54,7 +58,7 @@ export class AlarmCommentComponent implements OnInit {
alarmId: string;
@Input()
commentsHeaderEnabled: boolean = true;
alarmActivityOnly: boolean = false;
authUser: AuthUser;
@ -85,7 +89,9 @@ export class AlarmCommentComponent implements OnInit {
public fb: FormBuilder,
private dialogService: DialogService,
public dateAgoPipe: DateAgoPipe,
private utilsService: UtilsService) {
private utilsService: UtilsService,
private datePipe: DatePipe,
private importExportService: ImportExportService) {
this.authUser = getCurrentAuthUser(store);
@ -110,7 +116,8 @@ export class AlarmCommentComponent implements OnInit {
this.alarmComments = pagedData.data;
this.displayData.length = 0;
for (let alarmComment of pagedData.data) {
let displayDataElement: AlarmCommentsDisplayData = {};
let displayDataElement = {} as AlarmCommentsDisplayData;
displayDataElement.createdTime = this.datePipe.transform(alarmComment.createdTime, 'yyyy-MM-dd HH:mm:ss');
displayDataElement.createdDateAgo = this.dateAgoPipe.transform(alarmComment.createdTime);
displayDataElement.commentText = alarmComment.comment.text;
displayDataElement.isSystemComment = alarmComment.type === AlarmCommentType.SYSTEM;
@ -119,7 +126,8 @@ export class AlarmCommentComponent implements OnInit {
displayDataElement.displayName = this.getUserDisplayName(alarmComment);
displayDataElement.edit = false;
displayDataElement.isEdited = alarmComment.comment.edited;
displayDataElement.editedDateAgo = this.dateAgoPipe.transform(alarmComment.comment.editedOn).toLowerCase();
displayDataElement.editedTime = this.datePipe.transform(alarmComment.comment.editedOn, 'yyyy-MM-dd HH:mm:ss');
displayDataElement.editedDateAgo = this.dateAgoPipe.transform(alarmComment.comment.editedOn) + '\n';
displayDataElement.showActions = false;
displayDataElement.isSystemComment = false;
displayDataElement.avatarBgColor = this.utilsService.stringToHslColor(displayDataElement.displayName,
@ -137,6 +145,11 @@ export class AlarmCommentComponent implements OnInit {
this.loadAlarmComments();
}
exportAlarmActivity() {
let fileName = this.translate.instant('alarm.alarm') + '_' + this.translate.instant('alarm-activity.activity');
this.importExportService.exportCsv(this.getDataForExport(), fileName.toLowerCase());
}
saveComment(): void {
const commentInputValue: string = this.getAlarmCommentFormControl().value;
if (commentInputValue) {
@ -194,7 +207,7 @@ export class AlarmCommentComponent implements OnInit {
const alarmCommentInfo: AlarmComment = this.getAlarmCommentById(commentId);
const commentText: string = alarmCommentInfo.comment.text;
this.dialogService.confirm(
this.translate.instant('alarm-comment.delete-alarm-comment'),
this.translate.instant('alarm-activity.delete-alarm-comment'),
commentText,
this.translate.instant('action.cancel'),
this.translate.instant('action.delete')).subscribe(
@ -211,17 +224,19 @@ export class AlarmCommentComponent implements OnInit {
}
getSortDirectionIcon() {
return this.alarmCommentSortOrder.direction === Direction.DESC ? 'arrow_downward' : 'arrow_upward'
return this.alarmCommentSortOrder.direction === Direction.DESC ? 'mdi:sort-descending' : 'mdi:sort-ascending'
}
getSortDirectionTooltipText() {
let text = this.alarmCommentSortOrder.direction === Direction.DESC ? 'alarm-activity.newest-first' :
'alarm-activity.oldest-first';
return this.translate.instant(text);
}
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;
@ -292,4 +307,19 @@ export class AlarmCommentComponent implements OnInit {
return this.displayData.find(commentDisplayData => commentDisplayData.commentId === commentId);
}
private getDataForExport() {
let dataToExport = [];
for (let row of this.displayData) {
let exportRow = {
[this.translate.instant('alarm-activity.author')]: row.isSystemComment ?
this.translate.instant('alarm-activity.system') : row.displayName,
[this.translate.instant('alarm-activity.created-date')]: row.createdTime,
[this.translate.instant('alarm-activity.edited-date')]: row.editedTime,
[this.translate.instant('alarm-activity.text')]: row.commentText
}
dataToExport.push(exportRow)
}
return dataToExport;
}
}

View File

@ -28,23 +28,13 @@
<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: 24px 0 0 0">
<fieldset [disabled]="isLoading$ | async" style="padding: 0 24px">
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" style="margin-bottom: 22px">
<div fxLayout="row" fxLayoutGap="6px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.originator</mat-label>
<input matInput formControlName="originatorName" readonly>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.created-time</mat-label>
<input matInput formControlName="createdTime" readonly>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="6px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.type</mat-label>
<input matInput formControlName="type" readonly>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.severity</mat-label>
<input matInput formControlName="alarmSeverity" readonly
@ -52,66 +42,50 @@
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="6px">
<mat-form-field *ngIf="alarmFormGroup.get('startTime').value" fxFlex class="mat-block">
<mat-label translate>alarm.start-time</mat-label>
<input matInput formControlName="startTime" readonly>
</mat-form-field>
<mat-form-field *ngIf="alarmFormGroup.get('duration').value" fxFlex class="mat-block">
<mat-label translate>alarm.duration</mat-label>
<input matInput formControlName="duration" readonly>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="6px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.type</mat-label>
<input matInput formControlName="type" readonly>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.status</mat-label>
<input matInput formControlName="alarmStatus" 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">
<mat-expansion-panel-header fxLayout="row wrap" style="margin-bottom: 8px">
<mat-panel-title>
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
alarm.advanced-info
<mat-panel-description fxLayoutAlign="end center" translate>
alarm.show-more
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxLayout="row" fxLayoutGap="6px" *ngIf="alarmFormGroup.get('startTime').value ||
alarmFormGroup.get('endTime').value">
<mat-form-field *ngIf="alarmFormGroup.get('startTime').value" fxFlex class="mat-block">
<mat-label translate>alarm.start-time</mat-label>
<input matInput formControlName="startTime" readonly>
</mat-form-field>
<mat-form-field *ngIf="alarmFormGroup.get('endTime').value" fxFlex class="mat-block">
<mat-label translate>alarm.end-time</mat-label>
<input matInput formControlName="endTime" readonly>
</mat-form-field>
<span fxFlex *ngIf="!alarmFormGroup.get('startTime').value || !alarmFormGroup.get('endTime').value"></span>
</div>
<div fxLayout="row" fxLayoutGap="6px" *ngIf="alarmFormGroup.get('ackTime').value ||
alarmFormGroup.get('clearTime').value">
<mat-form-field *ngIf="alarmFormGroup.get('ackTime').value" fxFlex class="mat-block">
<mat-label translate>alarm.ack-time</mat-label>
<input matInput formControlName="ackTime" readonly>
</mat-form-field>
<mat-form-field *ngIf="alarmFormGroup.get('clearTime').value" fxFlex class="mat-block">
<mat-label translate>alarm.clear-time</mat-label>
<input matInput formControlName="clearTime" readonly>
</mat-form-field>
<span fxFlex *ngIf="!alarmFormGroup.get('ackTime').value || !alarmFormGroup.get('clearTime').value"></span>
</div>
<tb-json-object-edit
*ngIf="displayDetails"
formControlName="alarmDetails"
readonly
label="{{ 'alarm.details' | translate }}">
label="{{ 'alarm.additional-info' | translate }}">
</tb-json-object-edit>
</ng-template>
</mat-expansion-panel>
</fieldset>
<tb-alarm-assignee fxFlex [allowAssign]="allowAssign"
[alarm]="alarm$ | async"
(alarmReassigned)="onReassign()">
</tb-alarm-assignee>
<tb-alarm-comment #alarmCommentComponent [alarmId]="alarmId"></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>
<div fxLayout="row" *ngIf="alarm$ | async; let alarm;" fxLayoutGap="8px">
<button *ngIf="allowAcknowledgment && (alarm.status === alarmStatuses.ACTIVE_UNACK ||

View File

@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
.assignee-field {
.mat-form-field-label-wrapper {

View File

@ -34,6 +34,7 @@ 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 { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
export interface AlarmDetailsDialogData {
alarmId?: string;
@ -41,6 +42,7 @@ export interface AlarmDetailsDialogData {
allowAcknowledgment: boolean;
allowClear: boolean;
displayDetails: boolean;
allowAssign: boolean;
}
@Component({
@ -56,6 +58,7 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
allowAcknowledgment: boolean;
allowClear: boolean;
displayDetails: boolean;
allowAssign: boolean;
loadAlarmSubject = new ReplaySubject<AlarmInfo>();
alarm$: Observable<AlarmInfo> = this.loadAlarmSubject.asObservable().pipe(
@ -72,6 +75,7 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
constructor(protected store: Store<AppState>,
protected router: Router,
private datePipe: DatePipe,
private millisecondsToTimeStringPipe: MillisecondsToTimeStringPipe,
private translate: TranslateService,
@Inject(MAT_DIALOG_DATA) public data: AlarmDetailsDialogData,
private alarmService: AlarmService,
@ -82,17 +86,15 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
this.allowAcknowledgment = data.allowAcknowledgment;
this.allowClear = data.allowClear;
this.displayDetails = data.displayDetails;
this.allowAssign = data.allowAssign;
this.alarmFormGroup = this.fb.group(
{
createdTime: [''],
originatorName: [''],
startTime: [''],
endTime: [''],
ackTime: [''],
clearTime: [''],
type: [''],
alarmSeverity: [''],
startTime: [''],
duration: [''],
type: [''],
alarmStatus: [''],
alarmDetails: [null]
}
@ -116,29 +118,25 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
}
loadAlarmFields(alarm: AlarmInfo) {
this.alarmFormGroup.get('createdTime')
.patchValue(this.datePipe.transform(alarm.createdTime, 'yyyy-MM-dd HH:mm:ss'));
this.alarmFormGroup.get('originatorName')
.patchValue(alarm.originatorName);
.patchValue(alarm.originatorLabel ? alarm.originatorLabel : alarm.originatorName);
this.alarmFormGroup.get('alarmSeverity')
.patchValue(this.translate.instant(alarmSeverityTranslations.get(alarm.severity)));
if (alarm.startTs) {
this.alarmFormGroup.get('startTime')
.patchValue(this.datePipe.transform(alarm.startTs, 'yyyy-MM-dd HH:mm:ss'));
}
if (alarm.endTs) {
this.alarmFormGroup.get('endTime')
.patchValue(this.datePipe.transform(alarm.endTs, 'yyyy-MM-dd HH:mm:ss'));
}
if (alarm.ackTs) {
this.alarmFormGroup.get('ackTime')
.patchValue(this.datePipe.transform(alarm.ackTs, 'yyyy-MM-dd HH:mm:ss'));
}
if (alarm.clearTs) {
this.alarmFormGroup.get('clearTime')
.patchValue(this.datePipe.transform(alarm.clearTs, 'yyyy-MM-dd HH:mm:ss'));
if (alarm.startTs || alarm.endTs) {
let duration = '';
if (alarm.startTs && (alarm.status === AlarmStatus.ACTIVE_ACK || alarm.status === AlarmStatus.ACTIVE_UNACK)) {
duration = this.millisecondsToTimeStringPipe.transform(Date.now() - alarm.startTs);
}
if (alarm.endTs && (alarm.status === AlarmStatus.CLEARED_ACK || alarm.status === AlarmStatus.CLEARED_UNACK)) {
duration = this.millisecondsToTimeStringPipe.transform(alarm.endTs - alarm.startTs);
}
this.alarmFormGroup.get('duration').patchValue(duration);
}
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)));
this.alarmFormGroup.get('alarmDetails').patchValue(alarm.details);

View File

@ -53,7 +53,8 @@ import { Authority } from '@shared/models/authority.enum';
import { ChangeDetectorRef, Injector, StaticProvider, ViewContainerRef } from '@angular/core';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import {
ALARM_ASSIGNEE_PANEL_DATA, AlarmAssigneePanelComponent,
ALARM_ASSIGNEE_PANEL_DATA,
AlarmAssigneePanelComponent,
AlarmAssigneePanelData
} from '@home/components/alarm/alarm-assignee-panel.component';
import { ComponentPortal } from '@angular/cdk/portal';
@ -168,7 +169,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
}
showAlarmDetails(entity: AlarmInfo) {
const isPermissionWrite = this.authUser.authority !== Authority.CUSTOMER_USER || entity.customerId.id === this.authUser.customerId;
const isPermissionWrite = this.authUser.authority !== Authority.CUSTOMER_USER || entity.customerId?.id === this.authUser.customerId;
this.dialog.open<AlarmDetailsDialogComponent, AlarmDetailsDialogData, boolean>
(AlarmDetailsDialogComponent,
{
@ -179,7 +180,8 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
alarm: entity,
allowAcknowledgment: isPermissionWrite,
allowClear: isPermissionWrite,
displayDetails: true
displayDetails: true,
allowAssign: true
}
}).afterClosed().subscribe(
(res) => {
@ -281,8 +283,13 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(AlarmAssigneePanelComponent,
this.viewContainerRef, injector)).onDestroy(() => this.updateData());
const componentRef = overlayRef.attach(new ComponentPortal(AlarmAssigneePanelComponent,
this.viewContainerRef, injector));
componentRef.onDestroy(() => {
if (componentRef.instance.reassigned) {
this.updateData()
}
});
}
}

View File

@ -14,42 +14,44 @@
* limitations under the License.
*/
:host ::ng-deep {
tb-entities-table.tb-details-mode {
.mat-drawer-container {
background-color: white;
.assignee-cell {
display: flex;
justify-content: flex-start;
align-items: center;
.assigned-container {
max-width: 180px;
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 {
tb-entities-table {
&.tb-details-mode {
.mat-drawer-container {
background-color: white;
}
}
.assignee-cell {
display: flex;
justify-content: flex-start;
align-items: center;
.assigned-container {
max-width: 180px;
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;
font-size: 28px;
margin-right: 8px;
color: rgba(0, 0, 0, 0.38);
overflow: visible;
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;
}
}
}
}

View File

@ -150,6 +150,11 @@ export const ZIP_TYPE: FileType = {
extension: 'zip'
};
export const CSV_TYPE: FileType = {
mimeType: 'attachament/csv',
extension: 'csv'
};
export function convertCSVToJson(csvdata: string, config: CsvToJsonConfig,
onError: (messageId: string, params?: any) => void): CsvToJsonResult | number {
config = config || {};

View File

@ -47,6 +47,7 @@ import { ItemBufferService, WidgetItem } from '@core/services/item-buffer.servic
import {
BulkImportRequest,
BulkImportResult,
CSV_TYPE,
FileType,
ImportWidgetResult,
JSON_TYPE,
@ -597,6 +598,35 @@ export class ImportExportService {
);
}
private processCSVCell(cellData: any): any {
if (isString(cellData)) {
let result = cellData.replace(/"/g, '""');
if (result.search(/([",\n])/g) >= 0) {
result = `"${result}"`;
}
return result;
}
return cellData;
}
public exportCsv(data: {[key: string]: any}[], filename: string) {
let colsHead: string;
let colsData: string;
if (data && data.length) {
colsHead = Object.keys(data[0]).map(key => [this.processCSVCell(key)]).join(';');
colsData = data.map(obj => [
Object.keys(obj).map(col => [
this.processCSVCell(obj[col])
]).join(';')
]).join('\n');
} else {
colsHead = '';
colsData = '';
}
const csvData = `${colsHead}\n${colsData}`;
this.downloadFile(csvData, filename, CSV_TYPE);
}
public exportText(data: string | Array<string>, filename: string) {
let content = data;
if (Array.isArray(data)) {

View File

@ -143,7 +143,7 @@ interface AlarmsTableWidgetSettings extends TableWidgetSettings {
enableSelection: boolean;
enableStatusFilter?: boolean;
enableFilter: boolean;
displayComments: boolean;
displayActivity: boolean;
displayDetails: boolean;
allowAcknowledgment: boolean;
allowClear: boolean;
@ -154,7 +154,7 @@ interface AlarmWidgetActionDescriptor extends TableCellButtonActionDescriptor {
details?: boolean;
acknowledge?: boolean;
clear?: boolean;
comments?: boolean;
activity?: boolean;
}
@Component({
@ -197,7 +197,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
private alarmsTitlePattern: string;
private displayComments = false;
private displayActivity = false;
private displayDetails = true;
public allowAcknowledgment = true;
private allowClear = true;
@ -335,7 +335,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
private initializeConfig() {
this.ctx.widgetActions = [this.searchAction, this.alarmFilterAction, this.columnDisplayAction];
this.displayComments = isDefined(this.settings.displayComments) ? this.settings.displayComments : false;
this.displayActivity = isDefined(this.settings.displayActivity) ? this.settings.displayActivity : false;
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;
@ -464,12 +464,12 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.sortOrderProperty = sortColumn ? sortColumn.def : null;
const actionCellDescriptors: AlarmWidgetActionDescriptor[] = [];
if (this.displayComments) {
if (this.displayActivity) {
actionCellDescriptors.push(
{
displayName: this.translate.instant('alarm-comment.comments'),
displayName: this.translate.instant('alarm-activity.activity'),
icon: 'comment',
comments: true
activity: true
} as AlarmWidgetActionDescriptor
);
}
@ -821,8 +821,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 (actionDescriptor.activity) {
this.openAlarmActivity($event, alarm);
} else {
if ($event) {
$event.stopPropagation();
@ -864,7 +864,8 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
alarmId: alarm.id.id,
allowAcknowledgment: this.allowAcknowledgment,
allowClear: this.allowClear,
displayDetails: true
displayDetails: true,
allowAssign: this.allowAssign
}
}).afterClosed().subscribe(
(res) => {
@ -988,7 +989,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
}
}
private openAlarmComments($event: Event, alarm: AlarmDataInfo) {
private openAlarmActivity($event: Event, alarm: AlarmDataInfo) {
if ($event) {
$event.stopPropagation();
}
@ -999,8 +1000,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
alarmId: alarm.id.id,
commentsHeaderEnabled: false
alarmId: alarm.id.id
}
}).afterClosed()
}

View File

@ -58,8 +58,8 @@
</mat-select>
</mat-form-field>
<section fxLayout="column" fxLayoutGap="8px">
<mat-slide-toggle formControlName="displayComments">
{{ 'widgets.table.display-alarm-comments' | translate }}
<mat-slide-toggle formControlName="displayActivity">
{{ 'widgets.table.display-alarm-activity' | translate }}
</mat-slide-toggle>
<mat-slide-toggle fxFlex formControlName="displayDetails">
{{ 'widgets.table.display-alarm-details' | translate }}

View File

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

View File

@ -466,6 +466,7 @@
"end-time": "End time",
"ack-time": "Acknowledged time",
"clear-time": "Cleared time",
"duration": "Duration",
"alarm-severity-list": "Alarm severity list",
"any-severity": "Any severity",
"severity-critical": "Critical",
@ -501,15 +502,24 @@
"any-type": "Any type",
"search-propagated-alarms": "Search propagated alarms",
"comments": "Alarm comments",
"advanced-info": "Advanced info"
"show-more": "Show more",
"additional-info": "Additional info"
},
"alarm-comment": {
"alarm-activity": {
"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"
"oldest-first": "Oldest first",
"newest-first": "Newest first",
"activity": "Activity",
"export": "Export to CSV",
"author": "Author",
"created-date": "Created date",
"edited-date": "Edited date",
"text": "Text",
"system": "System"
},
"alias": {
"add": "Add alias",
@ -5042,7 +5052,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",
"display-alarm-activity": "Display alarm activity",
"allow-alarms-assign": "Allow alarms assignment"
},
"value-source": {