22 changed files with 421 additions and 314 deletions
@ -0,0 +1,21 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<tb-notification |
|||
[notification]="notification" |
|||
(markAsRead)="markAsRead($event)" |
|||
></tb-notification> |
|||
@ -0,0 +1,21 @@ |
|||
/** |
|||
* 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{ |
|||
display: block; |
|||
max-width: 400px; |
|||
width: 350px; |
|||
padding: 2px; |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
///
|
|||
/// 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 } from '@angular/core'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Notification } from '@shared/models/notification.models'; |
|||
|
|||
export interface InboxNotificationDialogData { |
|||
notification: Notification; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-inbox-notification-dialog', |
|||
templateUrl: './inbox-notification-dialog.component.html', |
|||
styleUrls: ['inbox-notification-dialog.component.scss'] |
|||
}) |
|||
export class InboxNotificationDialogComponent extends DialogComponent<InboxNotificationDialogComponent, string> { |
|||
|
|||
notification: Notification; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
protected dialogRef: MatDialogRef<InboxNotificationDialogComponent, string>, |
|||
@Inject(MAT_DIALOG_DATA) public data: InboxNotificationDialogData) { |
|||
super(store, router, dialogRef); |
|||
|
|||
this.notification = data.notification; |
|||
} |
|||
|
|||
markAsRead(id: string) { |
|||
this.dialogRef.close(id) |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<mat-toolbar color="primary"> |
|||
<h2 translate>notification.failed-send</h2> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<div mat-dialog-content> |
|||
<div *ngFor="let errorStat of errorStats | keyvalue; last as isLast"> |
|||
<h6>{{ notificationDeliveryMethodErrorTranslateMap.get(errorStat.key) | translate }}</h6> |
|||
<table class="tb-table-list"> |
|||
<tr *ngFor="let error of errorStat.value | keyvalue"> |
|||
<td> |
|||
<mat-chip>{{ error.key }}</mat-chip> |
|||
</td> |
|||
<td class="tb-row-error">{{ error.value }}</td> |
|||
</tr> |
|||
</table> |
|||
<mat-divider *ngIf="!isLast"></mat-divider> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,48 @@ |
|||
/** |
|||
* 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{ |
|||
display: block; |
|||
width: 600px; |
|||
max-width: 100%; |
|||
|
|||
h6 { |
|||
font-weight: 500; |
|||
font-size: 16px; |
|||
line-height: 20px; |
|||
margin-top: 0; |
|||
} |
|||
|
|||
.tb-table-list { |
|||
table-layout: fixed; |
|||
width: 100%; |
|||
border: hidden; |
|||
border-collapse: collapse; |
|||
|
|||
td { |
|||
border: 8px solid transparent; |
|||
border-collapse: collapse; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.tb-row-error { |
|||
word-wrap: break-word; |
|||
} |
|||
} |
|||
|
|||
.mat-divider { |
|||
margin: 24px 0; |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
///
|
|||
/// 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 } from '@angular/core'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { NotificationDeliveryMethod, NotificationRequest } from '@shared/models/notification.models'; |
|||
|
|||
export interface NotificationRequestErrorDialogData { |
|||
notificationRequest: NotificationRequest; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-notification-request-error-dialog', |
|||
templateUrl: './notification-request-error-dialog.component.html', |
|||
styleUrls: ['notification-request-error-dialog.component.scss'] |
|||
}) |
|||
export class NotificationRequestErrorDialogComponent extends DialogComponent<NotificationRequestErrorDialogComponent, void> { |
|||
|
|||
errorStats: { [key in NotificationDeliveryMethod]: {[key in string]: string}}; |
|||
|
|||
notificationDeliveryMethodErrorTranslateMap = new Map<NotificationDeliveryMethod, string>([ |
|||
[NotificationDeliveryMethod.PUSH, 'notification.delivery-method-type.push-failed-sent'], |
|||
[NotificationDeliveryMethod.SMS, 'notification.delivery-method-type.sms-failed-sent'], |
|||
[NotificationDeliveryMethod.EMAIL, 'notification.delivery-method-type.email-failed-sent'], |
|||
[NotificationDeliveryMethod.SLACK, 'notification.delivery-method-type.slack-failed-sent'], |
|||
]); |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
protected dialogRef: MatDialogRef<NotificationRequestErrorDialogComponent, void>, |
|||
@Inject(MAT_DIALOG_DATA) public data: NotificationRequestErrorDialogData) { |
|||
super(store, router, dialogRef); |
|||
|
|||
this.errorStats = data.notificationRequest.stats.errors; |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
} |
|||
@ -1,63 +0,0 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<section [formGroup]="alarmSeveritiesForm"> |
|||
<mat-form-field fxFlex class="mat-block" [floatLabel]="flotLabel"> |
|||
<mat-label translate>alarm.alarm-severity-list</mat-label> |
|||
<mat-chip-grid #severitiesChipList formControlName="alarmSeverities" [required]="required"> |
|||
<mat-chip-row *ngFor="let severity of alarmSeverities" |
|||
[removable]="!disabled" |
|||
(removed)="onSeverityRemoved(severity)"> |
|||
{{ alarmSeverityTranslationMap.get(severity) | translate }} |
|||
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon> |
|||
</mat-chip-row> |
|||
<input matInput |
|||
type="text" |
|||
formControlName="alarmSeverity" |
|||
placeholder="{{ !alarmSeveritiesForm.get('alarmSeverities').value?.length ? ('alarm.any-severity' | translate) : '' }}" |
|||
#severityInput |
|||
(focusin)="onFocus()" |
|||
matAutocompleteOrigin |
|||
#origin="matAutocompleteOrigin" |
|||
[matAutocompleteConnectedTo]="origin" |
|||
[matAutocomplete]="severityAutocomplete" |
|||
[matChipInputFor]="severitiesChipList" |
|||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" |
|||
(matChipInputTokenEnd)="addSeverityFromChipInput($event)"> |
|||
</mat-chip-grid> |
|||
<mat-autocomplete #severityAutocomplete="matAutocomplete" |
|||
class="tb-autocomplete" |
|||
(optionSelected)="addSeverityFromAutocomplete($event)" |
|||
[displayWith]="displaySeverityFn.bind(this)"> |
|||
<mat-option *ngFor="let severity of filteredDisplaySeverities | async" [value]="severity"> |
|||
<span [innerHTML]="alarmSeverityTranslationMap.get(alarmSeverity[severity]) | translate | highlight:searchText"></span> |
|||
</mat-option> |
|||
<mat-option *ngIf="(filteredDisplaySeverities | async)?.length === 0" [value]="null" class="tb-not-found"> |
|||
<div class="tb-not-found-content" (click)="$event.stopPropagation()"> |
|||
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty"> |
|||
<span translate>notification.no-severity-found</span> |
|||
</div> |
|||
<ng-template #searchNotEmpty> |
|||
<span> |
|||
{{ 'notification.no-severity-matching' | translate :{severity: truncate.transform(searchText, true, 6, '...')} }} |
|||
</span> |
|||
</ng-template> |
|||
</div> |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
</mat-form-field> |
|||
</section> |
|||
@ -1,220 +0,0 @@ |
|||
///
|
|||
/// 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, ElementRef, forwardRef, Input, ViewChild } from '@angular/core'; |
|||
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; |
|||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
|||
import { MatChipInputEvent } from '@angular/material/chips'; |
|||
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; |
|||
import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { TruncatePipe } from '@shared/pipe/truncate.pipe'; |
|||
import { Observable } from 'rxjs'; |
|||
import { map, share } from 'rxjs/operators'; |
|||
import { MatAutocompleteSelectedEvent, MatAutocompleteTrigger } from '@angular/material/autocomplete'; |
|||
import { FloatLabelType } from '@angular/material/form-field'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-alarm-severities-list', |
|||
templateUrl: './alarm-severities-list.component.html', |
|||
styleUrls: [], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => AlarmSeveritiesListComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class AlarmSeveritiesListComponent implements ControlValueAccessor{ |
|||
|
|||
@ViewChild('severityInput', {static: true}) severityInput: ElementRef<HTMLInputElement>; |
|||
@ViewChild('severityInput', {read: MatAutocompleteTrigger, static: true}) autocompleteTrigger: MatAutocompleteTrigger; |
|||
|
|||
alarmSeveritiesForm: FormGroup; |
|||
|
|||
alarmSeverityTranslationMap = alarmSeverityTranslations; |
|||
private alarmSeveritiesValues = Object.keys(AlarmSeverity) as Array<AlarmSeverity>; |
|||
alarmSeverity = AlarmSeverity; |
|||
|
|||
alarmSeverities: Array<AlarmSeverity> = [] |
|||
|
|||
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; |
|||
|
|||
private modelValue: Array<string> | null; |
|||
|
|||
private requiredValue: boolean; |
|||
get required(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
@Input() |
|||
set required(value: boolean) { |
|||
const newVal = coerceBooleanProperty(value); |
|||
if (this.requiredValue !== newVal) { |
|||
this.requiredValue = newVal; |
|||
this.updateValidators(); |
|||
} |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
flotLabel: FloatLabelType = 'auto'; |
|||
|
|||
searchText = ''; |
|||
filteredDisplaySeverities: Observable<Array<string>>; |
|||
|
|||
private dirty = false; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private fb: FormBuilder, |
|||
private translate: TranslateService, |
|||
public truncate: TruncatePipe) { |
|||
this.alarmSeveritiesForm = this.fb.group({ |
|||
alarmSeverities: [null, this.required ? [Validators.required] : []], |
|||
alarmSeverity: [''] |
|||
}); |
|||
|
|||
this.filteredDisplaySeverities = this.alarmSeveritiesForm.get('alarmSeverity').valueChanges |
|||
.pipe( |
|||
map((value) => value ? value : ''), |
|||
map(name => this.fetchSeverities(name) ), |
|||
share() |
|||
); |
|||
} |
|||
|
|||
updateValidators() { |
|||
this.alarmSeveritiesForm.get('alarmSeverities').setValidators(this.required ? [Validators.required] : []); |
|||
this.alarmSeveritiesForm.get('alarmSeverities').updateValueAndValidity(); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.alarmSeveritiesForm.disable({emitEvent: false}); |
|||
} else { |
|||
this.alarmSeveritiesForm.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: Array<AlarmSeverity> | null): void { |
|||
this.searchText = ''; |
|||
if (value != null && value.length > 0) { |
|||
this.modelValue = [...value]; |
|||
this.alarmSeverities = [...value]; |
|||
this.alarmSeveritiesForm.get('alarmSeverities').setValue(value); |
|||
} else { |
|||
this.alarmSeverities = []; |
|||
this.alarmSeveritiesForm.get('alarmSeverities').setValue(null); |
|||
this.modelValue = null; |
|||
} |
|||
this.dirty = true; |
|||
} |
|||
|
|||
addSeverityFromAutocomplete(event: MatAutocompleteSelectedEvent) { |
|||
this.addSeverity(event.option.value); |
|||
} |
|||
|
|||
addSeverityFromChipInput(event: MatChipInputEvent): void { |
|||
const alarmSeverity = event.value || ''; |
|||
const result = this.fetchSeverities(alarmSeverity.trim()); |
|||
if (result.length === 1) { |
|||
this.addSeverity(result[0]); |
|||
} |
|||
} |
|||
|
|||
private addSeverity(alarmSeverity: AlarmSeverity) { |
|||
if (alarmSeverity) { |
|||
if (!this.modelValue || this.modelValue.indexOf(alarmSeverity) === -1) { |
|||
if (!this.modelValue) { |
|||
this.modelValue = []; |
|||
} |
|||
this.modelValue.push(alarmSeverity); |
|||
this.alarmSeverities.push(alarmSeverity); |
|||
this.alarmSeveritiesForm.get('alarmSeverities').setValue(this.modelValue); |
|||
} |
|||
this.propagateChange(this.modelValue); |
|||
if (!this.fetchSeverities().length) { |
|||
this.clear('', true); |
|||
this.autocompleteTrigger.closePanel(); |
|||
} else { |
|||
this.clear(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
onSeverityRemoved(alarmSeverity: string) { |
|||
const index = this.modelValue.indexOf(alarmSeverity); |
|||
if (index >= 0) { |
|||
this.modelValue.splice(index, 1); |
|||
this.alarmSeverities.splice(index, 1); |
|||
if (!this.modelValue.length) { |
|||
this.modelValue = null; |
|||
} |
|||
this.alarmSeveritiesForm.get('alarmSeverities').setValue(this.modelValue); |
|||
this.propagateChange(this.modelValue); |
|||
this.clear(this.severityInput.nativeElement.value, true); |
|||
this.autocompleteTrigger.closePanel(); |
|||
} |
|||
} |
|||
|
|||
displaySeverityFn(severity?: string): string | undefined { |
|||
return severity ? this.translate.instant(alarmSeverityTranslations.get(AlarmSeverity[severity])) : undefined; |
|||
} |
|||
|
|||
onFocus() { |
|||
if (this.dirty) { |
|||
this.alarmSeveritiesForm.get('alarmSeverity').updateValueAndValidity({onlySelf: true, emitEvent: true}); |
|||
this.dirty = false; |
|||
} |
|||
} |
|||
|
|||
textIsNotEmpty(text: string): boolean { |
|||
return (text && text.length > 0); |
|||
} |
|||
|
|||
private fetchSeverities(searchText?: string): Array<AlarmSeverity> { |
|||
this.searchText = searchText; |
|||
const allowAlarmSeverity = this.alarmSeveritiesValues.filter(severity => !this.modelValue?.includes(severity)); |
|||
if (this.textIsNotEmpty(this.searchText)) { |
|||
const search = this.searchText.toUpperCase(); |
|||
return allowAlarmSeverity.filter(severity => severity.toUpperCase().includes(search)); |
|||
} else { |
|||
return allowAlarmSeverity; |
|||
} |
|||
} |
|||
|
|||
private clear(value: string = '', ignoreFocus = false) { |
|||
this.severityInput.nativeElement.value = value; |
|||
this.alarmSeveritiesForm.get('alarmSeverity').patchValue(value); |
|||
if (!ignoreFocus) { |
|||
setTimeout(() => { |
|||
this.severityInput.nativeElement.blur(); |
|||
this.severityInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue