218 changed files with 24110 additions and 1661 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,39 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
package org.thingsboard.server.common.data.alarm; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Builder; |
|||
import lombok.Data; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
import org.thingsboard.server.common.data.page.TimePageLink; |
|||
|
|||
import java.util.List; |
|||
|
|||
@Data |
|||
@Builder |
|||
@AllArgsConstructor |
|||
public class AlarmQueryV2 { |
|||
|
|||
private EntityId affectedEntityId; |
|||
private TimePageLink pageLink; |
|||
private List<String> typeList; |
|||
private List<AlarmSearchStatus> statusList; |
|||
private List<AlarmSeverity> severityList; |
|||
private UserId assigneeId; |
|||
|
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
package org.thingsboard.server.common.data.exception; |
|||
|
|||
import lombok.Getter; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
|
|||
public class TenantNotFoundException extends RuntimeException { |
|||
|
|||
@Getter |
|||
private final TenantId tenantId; |
|||
|
|||
public TenantNotFoundException(TenantId tenantId) { |
|||
super("Tenant with id " + tenantId + " not found"); |
|||
this.tenantId = tenantId; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
package org.thingsboard.server.common.data.plugin; |
|||
|
|||
/** |
|||
* The main idea to use this - it's adding the ability to start rule nodes in singleton mode in cluster setup |
|||
* (singleton rule node will start in only one Rule Engine instance) |
|||
* USER_PREFERENCE - user has ability to configure clustering mode (enable/disable singleton mode in rule node config) |
|||
* ENABLE - user doesn't have ability to configure clustering mode (singleton mode is always FALSE in rule node config) |
|||
* SINGLETON - user doesn't have ability to configure clustering mode (singleton mode is always TRUE in rule node config) |
|||
*/ |
|||
public enum ComponentClusteringMode { |
|||
USER_PREFERENCE, |
|||
ENABLED, |
|||
SINGLETON |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
package org.thingsboard.server.common.msg.notification.trigger; |
|||
|
|||
import lombok.Builder; |
|||
import lombok.Data; |
|||
import org.thingsboard.server.common.data.HasName; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.audit.ActionType; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; |
|||
|
|||
@Data |
|||
@Builder |
|||
public class EntityActionTrigger implements NotificationRuleTrigger { |
|||
|
|||
private final TenantId tenantId; |
|||
private final EntityId entityId; |
|||
private final HasName entity; |
|||
private final ActionType actionType; |
|||
private final User user; |
|||
|
|||
@Override |
|||
public NotificationRuleTriggerType getType() { |
|||
return NotificationRuleTriggerType.ENTITY_ACTION; |
|||
} |
|||
|
|||
@Override |
|||
public EntityId getOriginatorEntityId() { |
|||
return entityId; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
///
|
|||
/// 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 { Observable } from 'rxjs'; |
|||
import { HttpClient } from '@angular/common/http'; |
|||
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; |
|||
import { UsageInfo } from '@shared/models/usage.models'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class UsageInfoService { |
|||
|
|||
constructor( |
|||
private http: HttpClient |
|||
) {} |
|||
|
|||
public getUsageInfo(config?: RequestConfig): Observable<UsageInfo> { |
|||
return this.http.get<UsageInfo>('/api/usage', |
|||
defaultHttpOptionsFromConfig(config)); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,203 @@ |
|||
///
|
|||
/// 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 { |
|||
AfterViewInit, |
|||
Component, |
|||
ElementRef, |
|||
Inject, |
|||
InjectionToken, |
|||
OnDestroy, |
|||
OnInit, |
|||
ViewChild |
|||
} from '@angular/core'; |
|||
import { FormBuilder, FormGroup } from '@angular/forms'; |
|||
import { Observable, of, Subject } from 'rxjs'; |
|||
import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, takeUntil, } from 'rxjs/operators'; |
|||
import { User, UserEmailInfo } from '@shared/models/user.model'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { UserService } from '@core/http/user.service'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { Direction } from '@shared/models/page/sort-order'; |
|||
import { emptyPageData } from '@shared/models/page/page-data'; |
|||
import { OverlayRef } from '@angular/cdk/overlay'; |
|||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
|
|||
export const ALARM_ASSIGNEE_SELECT_PANEL_DATA = new InjectionToken<any>('AlarmAssigneeSelectPanelData'); |
|||
|
|||
export interface AlarmAssigneeSelectPanelData { |
|||
assigneeId?: string; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-alarm-assignee-select-panel', |
|||
templateUrl: './alarm-assignee-panel.component.html', |
|||
styleUrls: ['./alarm-assignee-panel.component.scss'] |
|||
}) |
|||
export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit, OnDestroy { |
|||
|
|||
private dirty = false; |
|||
|
|||
assigneeId?: string; |
|||
|
|||
assigneeNotSetText = 'alarm.assignee-not-set'; |
|||
|
|||
selectUserFormGroup: FormGroup; |
|||
|
|||
@ViewChild('userInput', {static: true}) userInput: ElementRef; |
|||
|
|||
filteredUsers: Observable<Array<UserEmailInfo>>; |
|||
|
|||
searchText = ''; |
|||
|
|||
userSelected = false; |
|||
|
|||
result?: UserEmailInfo; |
|||
|
|||
private destroy$ = new Subject<void>(); |
|||
|
|||
constructor(@Inject(ALARM_ASSIGNEE_SELECT_PANEL_DATA) public data: AlarmAssigneeSelectPanelData, |
|||
public overlayRef: OverlayRef, |
|||
public translate: TranslateService, |
|||
private userService: UserService, |
|||
private fb: FormBuilder, |
|||
private utilsService: UtilsService) { |
|||
this.assigneeId = data.assigneeId; |
|||
this.selectUserFormGroup = this.fb.group({ |
|||
user: [null] |
|||
}); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.filteredUsers = this.selectUserFormGroup.get('user').valueChanges |
|||
.pipe( |
|||
debounceTime(150), |
|||
map(value => value ? (typeof value === 'string' ? value : '') : ''), |
|||
distinctUntilChanged(), |
|||
switchMap(name => this.fetchUsers(name)), |
|||
share(), |
|||
takeUntil(this.destroy$) |
|||
); |
|||
} |
|||
|
|||
ngAfterViewInit() { |
|||
setTimeout(() => { |
|||
this.userInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
} |
|||
|
|||
displayUserFn(user?: User): string | undefined { |
|||
return user ? user.email : undefined; |
|||
} |
|||
|
|||
selected(event: MatAutocompleteSelectedEvent): void { |
|||
this.clear(); |
|||
this.userSelected = true; |
|||
this.result = event.option.value; |
|||
this.overlayRef.dispose(); |
|||
} |
|||
|
|||
fetchUsers(searchText?: string): Observable<Array<UserEmailInfo>> { |
|||
this.searchText = searchText; |
|||
const pageLink = new PageLink(50, 0, searchText, { |
|||
property: 'email', |
|||
direction: Direction.ASC |
|||
}); |
|||
return this.userService.findUsersByQuery(pageLink, {ignoreLoading: true}) |
|||
.pipe( |
|||
catchError(() => of(emptyPageData<UserEmailInfo>())), |
|||
map(pageData => pageData.data) |
|||
); |
|||
} |
|||
|
|||
onFocus(): void { |
|||
if (!this.dirty) { |
|||
this.selectUserFormGroup.get('user').updateValueAndValidity({onlySelf: true}); |
|||
this.dirty = true; |
|||
} |
|||
} |
|||
|
|||
clear() { |
|||
this.selectUserFormGroup.get('user').patchValue('', {emitEvent: true}); |
|||
setTimeout(() => { |
|||
this.userInput.nativeElement.blur(); |
|||
this.userInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
getUserDisplayName(entity: UserEmailInfo) { |
|||
let displayName = ''; |
|||
if ((entity.firstName && entity.firstName.length > 0) || |
|||
(entity.lastName && entity.lastName.length > 0)) { |
|||
if (entity.firstName) { |
|||
displayName += entity.firstName; |
|||
} |
|||
if (entity.lastName) { |
|||
if (displayName.length > 0) { |
|||
displayName += ' '; |
|||
} |
|||
displayName += entity.lastName; |
|||
} |
|||
} else { |
|||
displayName = entity.email; |
|||
} |
|||
return displayName; |
|||
} |
|||
|
|||
getUserInitials(entity: UserEmailInfo): string { |
|||
let initials = ''; |
|||
if (entity.firstName && entity.firstName.length || |
|||
entity.lastName && entity.lastName.length) { |
|||
if (entity.firstName) { |
|||
initials += entity.firstName.charAt(0); |
|||
} |
|||
if (entity.lastName) { |
|||
initials += entity.lastName.charAt(0); |
|||
} |
|||
} else { |
|||
initials += entity.email.charAt(0); |
|||
} |
|||
return initials.toUpperCase(); |
|||
} |
|||
|
|||
getFullName(entity: UserEmailInfo): string { |
|||
let fullName = ''; |
|||
if ((entity.firstName && entity.firstName.length > 0) || |
|||
(entity.lastName && entity.lastName.length > 0)) { |
|||
if (entity.firstName) { |
|||
fullName += entity.firstName; |
|||
} |
|||
if (entity.lastName) { |
|||
if (fullName.length > 0) { |
|||
fullName += ' '; |
|||
} |
|||
fullName += entity.lastName; |
|||
} |
|||
} |
|||
return fullName; |
|||
} |
|||
|
|||
getAvatarBgColor(entity: UserEmailInfo) { |
|||
return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<ng-container [formGroup]="assigneeFormGroup"> |
|||
<mat-form-field fxFlex class="mat-block" style="margin-bottom: 25px" |
|||
(click)="openAlarmAssigneeSelectPanel($event)" |
|||
subscriptSizing="dynamic"> |
|||
<mat-label translate>alarm.assignee</mat-label> |
|||
<input matInput readonly formControlName="assignee"> |
|||
<span *ngIf="assignee" matPrefix class="user-avatar" |
|||
[style.backgroundColor]="getAvatarBgColor()"> |
|||
{{ getUserInitials() }} |
|||
</span> |
|||
<mat-icon *ngIf="!assignee" matPrefix class="unassigned-icon">account_circle</mat-icon> |
|||
<mat-icon *ngIf="!disabled" matSuffix>arrow_drop_down</mat-icon> |
|||
</mat-form-field> |
|||
</ng-container> |
|||
@ -0,0 +1,193 @@ |
|||
///
|
|||
/// 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, forwardRef, Injector, Input, OnInit, StaticProvider, ViewContainerRef } from '@angular/core'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; |
|||
import { ComponentPortal } from '@angular/cdk/portal'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
|||
import { UserId } from '@shared/models/id/user-id'; |
|||
import { UserService } from '@core/http/user.service'; |
|||
import { User, UserEmailInfo } from '@shared/models/user.model'; |
|||
import { catchError, map, of } from 'rxjs'; |
|||
import { tap } from 'rxjs/operators'; |
|||
import { |
|||
ALARM_ASSIGNEE_SELECT_PANEL_DATA, |
|||
AlarmAssigneeSelectPanelComponent, |
|||
AlarmAssigneeSelectPanelData |
|||
} from '@home/components/alarm/alarm-assignee-select-panel.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-alarm-assignee-select', |
|||
templateUrl: './alarm-assignee-select.component.html', |
|||
styleUrls: ['./alarm-assignee.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => AlarmAssigneeSelectComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class AlarmAssigneeSelectComponent implements OnInit, ControlValueAccessor { |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
assigneeFormGroup: UntypedFormGroup; |
|||
assignee?: User | UserEmailInfo; |
|||
|
|||
private propagateChange = (_: any) => {}; |
|||
|
|||
constructor(private utilsService: UtilsService, |
|||
private overlay: Overlay, |
|||
private fb: UntypedFormBuilder, |
|||
private userService: UserService, |
|||
private viewContainerRef: ViewContainerRef, |
|||
private translateService: TranslateService) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.assigneeFormGroup = this.fb.group({ |
|||
assignee: [null, []] |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.assigneeFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.assigneeFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(userId?: UserId): void { |
|||
const userObservable = userId ? this.userService.getUser(userId.id, {ignoreErrors: true}).pipe( |
|||
catchError(() => of(null)) |
|||
) : of(null); |
|||
userObservable.pipe( |
|||
tap((user) => { |
|||
this.assignee = user; |
|||
}), |
|||
map((user) => this.getAssignee(user)) |
|||
).subscribe((assignee) => { |
|||
this.assigneeFormGroup.get('assignee').patchValue(assignee, {emitEvent: false}); |
|||
}); |
|||
} |
|||
|
|||
private getAssignee(user?: User| UserEmailInfo): string { |
|||
if (user) { |
|||
return this.getUserDisplayName(user); |
|||
} else { |
|||
return this.translateService.instant('alarm.assignee-not-set'); |
|||
} |
|||
} |
|||
|
|||
private getUserDisplayName(user?: User | UserEmailInfo): string { |
|||
let displayName = ''; |
|||
if ((user?.firstName && user?.firstName.length > 0) || |
|||
(user?.lastName && user?.lastName.length > 0)) { |
|||
if (user?.firstName) { |
|||
displayName += user?.firstName; |
|||
} |
|||
if (user?.lastName) { |
|||
if (displayName.length > 0) { |
|||
displayName += ' '; |
|||
} |
|||
displayName += user?.lastName; |
|||
} |
|||
} else { |
|||
displayName = user?.email; |
|||
} |
|||
return displayName; |
|||
} |
|||
|
|||
getUserInitials(): string { |
|||
let initials = ''; |
|||
if (this.assignee?.firstName && this.assignee?.firstName.length || |
|||
this.assignee?.lastName && this.assignee?.lastName.length) { |
|||
if (this.assignee?.firstName) { |
|||
initials += this.assignee?.firstName.charAt(0); |
|||
} |
|||
if (this.assignee?.lastName) { |
|||
initials += this.assignee?.lastName.charAt(0); |
|||
} |
|||
} else { |
|||
initials += this.assignee?.email.charAt(0); |
|||
} |
|||
return initials.toUpperCase(); |
|||
} |
|||
|
|||
getAvatarBgColor(): string { |
|||
return this.utilsService.stringToHslColor(this.getUserDisplayName(this.assignee), 40, 60); |
|||
} |
|||
|
|||
openAlarmAssigneeSelectPanel($event: Event) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
if (!this.disabled) { |
|||
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_SELECT_PANEL_DATA, |
|||
useValue: { |
|||
assigneeId: this.assignee?.id?.id |
|||
} as AlarmAssigneeSelectPanelData |
|||
}, |
|||
{ |
|||
provide: OverlayRef, |
|||
useValue: overlayRef |
|||
} |
|||
]; |
|||
const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); |
|||
const component = overlayRef.attach(new ComponentPortal(AlarmAssigneeSelectPanelComponent, |
|||
this.viewContainerRef, injector)); |
|||
component.onDestroy(() => { |
|||
if (component.instance.userSelected) { |
|||
this.assignee = component.instance.result; |
|||
this.assigneeFormGroup.get('assignee').patchValue(this.getAssignee(this.assignee), {emitEvent: false}); |
|||
this.propagateChange(this.assignee?.id); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<ng-container *ngIf="panelMode; else componentMode"> |
|||
<ng-container *ngTemplateOutlet="alarmFilterPanel"></ng-container> |
|||
</ng-container> |
|||
<ng-template #componentMode> |
|||
<ng-container *ngIf="buttonMode; else alarmFilter"> |
|||
<button color="primary" |
|||
matTooltip="{{buttonDisplayValue}}" |
|||
matTooltipPosition="above" |
|||
mat-stroked-button |
|||
(click)="toggleAlarmFilterPanel($event)"> |
|||
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}} |
|||
</button> |
|||
</ng-container> |
|||
</ng-template> |
|||
<ng-template #alarmFilterPanel> |
|||
<form fxLayout="column" class="mat-content mat-padding" (ngSubmit)="update()"> |
|||
<ng-container *ngTemplateOutlet="alarmFilter"></ng-container> |
|||
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center"> |
|||
<button type="button" |
|||
mat-button |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button type="submit" |
|||
mat-raised-button |
|||
color="primary" |
|||
[disabled]="alarmFilterConfigForm.invalid || !alarmFilterConfigForm.dirty"> |
|||
{{ 'action.update' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
</ng-template> |
|||
<ng-template #alarmFilter> |
|||
<div fxLayout="column" fxLayoutAlign="center" [formGroup]="alarmFilterConfigForm"> |
|||
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start start" |
|||
fxLayoutGap.gt-xs="8px"> |
|||
<mat-form-field fxFlex class="mat-block" floatLabel="always"> |
|||
<mat-label translate>alarm.alarm-status-list</mat-label> |
|||
<mat-select formControlName="statusList" multiple |
|||
placeholder="{{ !alarmFilterConfigForm.get('statusList').value?.length ? ('alarm.any-status' | translate) : '' }}"> |
|||
<mat-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus"> |
|||
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex class="mat-block" floatLabel="always"> |
|||
<mat-label translate>alarm.alarm-severity-list</mat-label> |
|||
<mat-select formControlName="severityList" multiple |
|||
placeholder="{{ !alarmFilterConfigForm.get('severityList').value?.length ? ('alarm.any-severity' | translate) : '' }}"> |
|||
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity"> |
|||
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start start" |
|||
fxLayoutGap.gt-xs="8px"> |
|||
<mat-form-field fxFlex class="mat-block" floatLabel="always"> |
|||
<mat-label translate>alarm.alarm-type-list</mat-label> |
|||
<mat-chip-grid #alarmTypeChipList formControlName="typeList"> |
|||
<mat-chip-row *ngFor="let type of alarmTypeList()" |
|||
[removable]="true" (removed)="removeAlarmType(type)"> |
|||
{{type}} |
|||
<mat-icon matChipRemove>cancel</mat-icon> |
|||
</mat-chip-row> |
|||
<input placeholder="{{ !alarmFilterConfigForm.get('typeList').value?.length ? ('alarm.any-type' | translate) : '' }}" |
|||
[matChipInputFor]="alarmTypeChipList" |
|||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" |
|||
matChipInputAddOnBlur |
|||
(matChipInputTokenEnd)="addAlarmType($event)"> |
|||
</mat-chip-grid> |
|||
</mat-form-field> |
|||
</div> |
|||
<div fxLayout="column" fxLayoutGap="16px"> |
|||
<mat-slide-toggle *ngIf="propagatedFilter" formControlName="searchPropagatedAlarms"> |
|||
{{ 'alarm.search-propagated-alarms' | translate }} |
|||
</mat-slide-toggle> |
|||
<mat-slide-toggle formControlName="assignedToCurrentUser"> |
|||
{{ (userMode ? 'alarm.assigned-to-me' : 'alarm.assigned-to-current-user') | translate }} |
|||
</mat-slide-toggle> |
|||
<tb-alarm-assignee-select |
|||
*ngIf="!alarmFilterConfigForm.get('assignedToCurrentUser').value" formControlName="assigneeId"> |
|||
</tb-alarm-assignee-select> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
@ -0,0 +1,318 @@ |
|||
///
|
|||
/// 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, |
|||
Inject, |
|||
InjectionToken, |
|||
Input, |
|||
OnDestroy, |
|||
OnInit, |
|||
Optional, |
|||
TemplateRef, |
|||
ViewChild, |
|||
ViewContainerRef |
|||
} from '@angular/core'; |
|||
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
|||
import { AlarmFilterConfig } from '@shared/models/query/query.models'; |
|||
import { coerceBoolean } from '@shared/decorators/coerce-boolean'; |
|||
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; |
|||
import { TemplatePortal } from '@angular/cdk/portal'; |
|||
import { |
|||
AlarmSearchStatus, |
|||
alarmSearchStatusTranslations, |
|||
AlarmSeverity, |
|||
alarmSeverityTranslations |
|||
} from '@shared/models/alarm.models'; |
|||
import { MatChipInputEvent } from '@angular/material/chips'; |
|||
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
|
|||
export const ALARM_FILTER_CONFIG_DATA = new InjectionToken<any>('AlarmFilterConfigData'); |
|||
|
|||
export interface AlarmFilterConfigData { |
|||
panelMode: boolean; |
|||
userMode: boolean; |
|||
alarmFilterConfig: AlarmFilterConfig; |
|||
} |
|||
|
|||
// @dynamic
|
|||
@Component({ |
|||
selector: 'tb-alarm-filter-config', |
|||
templateUrl: './alarm-filter-config.component.html', |
|||
styleUrls: ['./alarm-filter-config.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => AlarmFilterConfigComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlValueAccessor { |
|||
|
|||
@ViewChild('alarmFilterPanel') |
|||
alarmFilterPanel: TemplateRef<any>; |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
@coerceBoolean() |
|||
@Input() |
|||
buttonMode = true; |
|||
|
|||
@coerceBoolean() |
|||
@Input() |
|||
userMode = false; |
|||
|
|||
@coerceBoolean() |
|||
@Input() |
|||
propagatedFilter = true; |
|||
|
|||
panelMode = false; |
|||
|
|||
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; |
|||
|
|||
alarmSearchStatuses = [AlarmSearchStatus.ACTIVE, |
|||
AlarmSearchStatus.CLEARED, |
|||
AlarmSearchStatus.ACK, |
|||
AlarmSearchStatus.UNACK]; |
|||
|
|||
alarmSearchStatusTranslationMap = alarmSearchStatusTranslations; |
|||
|
|||
alarmSeverities = Object.keys(AlarmSeverity); |
|||
alarmSeverityEnum = AlarmSeverity; |
|||
|
|||
alarmSeverityTranslationMap = alarmSeverityTranslations; |
|||
|
|||
buttonDisplayValue = this.translate.instant('alarm.alarm-filter'); |
|||
|
|||
alarmFilterConfigForm: UntypedFormGroup; |
|||
|
|||
alarmFilterOverlayRef: OverlayRef; |
|||
|
|||
panelResult: AlarmFilterConfig = null; |
|||
|
|||
private alarmFilterConfig: AlarmFilterConfig; |
|||
|
|||
private propagateChange = (_: any) => {}; |
|||
|
|||
constructor(@Optional() @Inject(ALARM_FILTER_CONFIG_DATA) |
|||
private data: AlarmFilterConfigData | undefined, |
|||
@Optional() |
|||
private overlayRef: OverlayRef, |
|||
private fb: UntypedFormBuilder, |
|||
private translate: TranslateService, |
|||
private overlay: Overlay, |
|||
private nativeElement: ElementRef, |
|||
private viewContainerRef: ViewContainerRef) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
if (this.data) { |
|||
this.panelMode = this.data.panelMode; |
|||
this.userMode = this.data.userMode; |
|||
this.alarmFilterConfig = this.data.alarmFilterConfig; |
|||
} |
|||
this.alarmFilterConfigForm = this.fb.group({ |
|||
statusList: [null, []], |
|||
severityList: [null, []], |
|||
typeList: [null, []], |
|||
searchPropagatedAlarms: [false, []], |
|||
assignedToCurrentUser: [false, []], |
|||
assigneeId: [null, []] |
|||
}); |
|||
this.alarmFilterConfigForm.valueChanges.subscribe( |
|||
() => { |
|||
this.updateValidators(); |
|||
if (!this.buttonMode) { |
|||
this.alarmConfigUpdated(this.alarmFilterConfigForm.value); |
|||
} |
|||
} |
|||
); |
|||
if (this.panelMode) { |
|||
this.updateAlarmConfigForm(this.alarmFilterConfig); |
|||
} |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.alarmFilterConfigForm.disable({emitEvent: false}); |
|||
} else { |
|||
this.alarmFilterConfigForm.enable({emitEvent: false}); |
|||
this.updateValidators(); |
|||
} |
|||
} |
|||
|
|||
writeValue(alarmFilterConfig?: AlarmFilterConfig): void { |
|||
this.alarmFilterConfig = alarmFilterConfig; |
|||
this.updateButtonDisplayValue(); |
|||
this.updateAlarmConfigForm(alarmFilterConfig); |
|||
} |
|||
|
|||
private updateValidators() { |
|||
const assignedToCurrentUser = this.alarmFilterConfigForm.get('assignedToCurrentUser').value; |
|||
if (assignedToCurrentUser) { |
|||
this.alarmFilterConfigForm.get('assigneeId').disable({emitEvent: false}); |
|||
} else { |
|||
this.alarmFilterConfigForm.get('assigneeId').enable({emitEvent: false}); |
|||
} |
|||
this.alarmFilterConfigForm.get('assigneeId').updateValueAndValidity({emitEvent: false}); |
|||
} |
|||
|
|||
toggleAlarmFilterPanel($event: Event) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
const config = new OverlayConfig({ |
|||
panelClass: 'tb-alarm-filter-panel', |
|||
backdropClass: 'cdk-overlay-transparent-backdrop', |
|||
hasBackdrop: true, |
|||
maxHeight: '80vh', |
|||
height: 'min-content', |
|||
minWidth: '' |
|||
}); |
|||
config.hasBackdrop = true; |
|||
const connectedPosition: ConnectedPosition = { |
|||
originX: 'start', |
|||
originY: 'bottom', |
|||
overlayX: 'start', |
|||
overlayY: 'top' |
|||
}; |
|||
config.positionStrategy = this.overlay.position().flexibleConnectedTo(this.nativeElement) |
|||
.withPositions([connectedPosition]); |
|||
|
|||
this.alarmFilterOverlayRef = this.overlay.create(config); |
|||
this.alarmFilterOverlayRef.backdropClick().subscribe(() => { |
|||
this.alarmFilterOverlayRef.dispose(); |
|||
}); |
|||
this.alarmFilterOverlayRef.attach(new TemplatePortal(this.alarmFilterPanel, |
|||
this.viewContainerRef)); |
|||
} |
|||
|
|||
cancel() { |
|||
this.updateAlarmConfigForm(this.alarmFilterConfig); |
|||
if (this.overlayRef) { |
|||
this.overlayRef.dispose(); |
|||
} else { |
|||
this.alarmFilterOverlayRef.dispose(); |
|||
} |
|||
} |
|||
|
|||
update() { |
|||
this.alarmConfigUpdated(this.alarmFilterConfigForm.value); |
|||
if (this.panelMode) { |
|||
this.panelResult = this.alarmFilterConfig; |
|||
} |
|||
if (this.overlayRef) { |
|||
this.overlayRef.dispose(); |
|||
} else { |
|||
this.alarmFilterOverlayRef.dispose(); |
|||
} |
|||
} |
|||
|
|||
public alarmTypeList(): string[] { |
|||
return this.alarmFilterConfigForm.get('typeList').value; |
|||
} |
|||
|
|||
public removeAlarmType(type: string): void { |
|||
const types: string[] = this.alarmFilterConfigForm.get('typeList').value; |
|||
const index = types.indexOf(type); |
|||
if (index >= 0) { |
|||
types.splice(index, 1); |
|||
this.alarmFilterConfigForm.get('typeList').setValue(types); |
|||
this.alarmFilterConfigForm.get('typeList').markAsDirty(); |
|||
} |
|||
} |
|||
|
|||
public addAlarmType(event: MatChipInputEvent): void { |
|||
const input = event.chipInput.inputElement; |
|||
const value = event.value; |
|||
|
|||
let types: string[] = this.alarmFilterConfigForm.get('typeList').value; |
|||
|
|||
if ((value || '').trim()) { |
|||
if (!types) { |
|||
types = []; |
|||
} |
|||
types.push(value.trim()); |
|||
this.alarmFilterConfigForm.get('typeList').setValue(types); |
|||
this.alarmFilterConfigForm.get('typeList').markAsDirty(); |
|||
} |
|||
|
|||
if (input) { |
|||
input.value = ''; |
|||
} |
|||
} |
|||
|
|||
private updateAlarmConfigForm(alarmFilterConfig?: AlarmFilterConfig) { |
|||
this.alarmFilterConfigForm.patchValue({ |
|||
statusList: alarmFilterConfig?.statusList, |
|||
severityList: alarmFilterConfig?.severityList, |
|||
typeList: alarmFilterConfig?.typeList, |
|||
searchPropagatedAlarms: alarmFilterConfig?.searchPropagatedAlarms, |
|||
assignedToCurrentUser: alarmFilterConfig?.assignedToCurrentUser, |
|||
assigneeId: alarmFilterConfig?.assigneeId |
|||
}, {emitEvent: false}); |
|||
this.updateValidators(); |
|||
} |
|||
|
|||
private alarmConfigUpdated(alarmFilterConfig: AlarmFilterConfig) { |
|||
this.alarmFilterConfig = alarmFilterConfig; |
|||
this.updateButtonDisplayValue(); |
|||
this.propagateChange(this.alarmFilterConfig); |
|||
} |
|||
|
|||
private updateButtonDisplayValue() { |
|||
if (this.buttonMode) { |
|||
const filterTextParts: string[] = []; |
|||
if (this.alarmFilterConfig?.statusList?.length) { |
|||
filterTextParts.push(this.alarmFilterConfig.statusList.map(s => |
|||
this.translate.instant(alarmSearchStatusTranslations.get(s))).join(', ')); |
|||
} |
|||
if (this.alarmFilterConfig?.severityList?.length) { |
|||
filterTextParts.push(this.alarmFilterConfig.severityList.map(s => |
|||
this.translate.instant(alarmSeverityTranslations.get(s))).join(', ')); |
|||
} |
|||
if (this.alarmFilterConfig?.typeList?.length) { |
|||
filterTextParts.push(this.alarmFilterConfig.typeList.join(', ')); |
|||
} |
|||
if (this.alarmFilterConfig?.assignedToCurrentUser) { |
|||
filterTextParts.push(this.translate.instant('alarm.assigned-to-me')); |
|||
} else if (this.alarmFilterConfig?.assigneeId) { |
|||
filterTextParts.push(this.translate.instant('alarm.assigned')); |
|||
} |
|||
if (!filterTextParts.length) { |
|||
this.buttonDisplayValue = this.translate.instant('alarm.alarm-filter-title'); |
|||
} else { |
|||
this.buttonDisplayValue = this.translate.instant('alarm.filter-title') + `: ${filterTextParts.join(', ')}`; |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue