239 changed files with 17619 additions and 8946 deletions
@ -1,50 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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.rule.engine.util; |
|||
|
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import org.thingsboard.rule.engine.api.TbContext; |
|||
import org.thingsboard.rule.engine.api.TbNodeException; |
|||
import org.thingsboard.server.common.data.HasCustomerId; |
|||
import org.thingsboard.server.common.data.id.AssetId; |
|||
import org.thingsboard.server.common.data.id.CustomerId; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
|
|||
public class EntitiesCustomerIdAsyncLoader { |
|||
|
|||
public static ListenableFuture<CustomerId> findEntityIdAsync(TbContext ctx, EntityId originator) { |
|||
switch (originator.getEntityType()) { |
|||
case CUSTOMER: |
|||
return Futures.immediateFuture((CustomerId) originator); |
|||
case USER: |
|||
return toCustomerIdAsync(ctx, ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) originator)); |
|||
case ASSET: |
|||
return toCustomerIdAsync(ctx, ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) originator)); |
|||
case DEVICE: |
|||
return toCustomerIdAsync(ctx, Futures.immediateFuture(ctx.getDeviceService().findDeviceById(ctx.getTenantId(), (DeviceId) originator))); |
|||
default: |
|||
return Futures.immediateFailedFuture(new TbNodeException("Unexpected originator EntityType: " + originator.getEntityType())); |
|||
} |
|||
} |
|||
|
|||
private static <T extends HasCustomerId> ListenableFuture<CustomerId> toCustomerIdAsync(TbContext ctx, ListenableFuture<T> future) { |
|||
return Futures.transform(future, in -> in != null ? in.getCustomerId() : null, ctx.getDbCallbackExecutor()); |
|||
} |
|||
|
|||
} |
|||
@ -1,154 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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.rule.engine.util; |
|||
|
|||
import com.google.common.util.concurrent.Futures; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.thingsboard.common.util.ListeningExecutor; |
|||
import org.thingsboard.rule.engine.TestDbCallbackExecutor; |
|||
import org.thingsboard.rule.engine.api.TbContext; |
|||
import org.thingsboard.rule.engine.api.TbNodeException; |
|||
import org.thingsboard.server.common.data.Customer; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.asset.Asset; |
|||
import org.thingsboard.server.common.data.id.AssetId; |
|||
import org.thingsboard.server.common.data.id.CustomerId; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.EntityIdFactory; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
import org.thingsboard.server.dao.asset.AssetService; |
|||
import org.thingsboard.server.dao.device.DeviceService; |
|||
import org.thingsboard.server.dao.user.UserService; |
|||
|
|||
import java.util.EnumSet; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.ExecutionException; |
|||
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals; |
|||
import static org.junit.jupiter.api.Assertions.assertInstanceOf; |
|||
import static org.junit.jupiter.api.Assertions.assertThrows; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.Mockito.doReturn; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
public class EntitiesCustomerIdAsyncLoaderTest { |
|||
|
|||
private static final EnumSet<EntityType> SUPPORTED_ENTITY_TYPES = EnumSet.of( |
|||
EntityType.CUSTOMER, |
|||
EntityType.USER, |
|||
EntityType.ASSET, |
|||
EntityType.DEVICE |
|||
); |
|||
private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); |
|||
@Mock |
|||
private TbContext ctxMock; |
|||
@Mock |
|||
private UserService userServiceMock; |
|||
@Mock |
|||
private AssetService assetServiceMock; |
|||
@Mock |
|||
private DeviceService deviceServiceMock; |
|||
|
|||
@Test |
|||
public void givenCustomerEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException { |
|||
// GIVEN
|
|||
var customer = new Customer(new CustomerId(UUID.randomUUID())); |
|||
|
|||
// WHEN
|
|||
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, customer.getId()).get(); |
|||
|
|||
// THEN
|
|||
assertEquals(customer.getId(), actualCustomerId); |
|||
} |
|||
|
|||
@Test |
|||
public void givenUserEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException { |
|||
// GIVEN
|
|||
var user = new User(new UserId(UUID.randomUUID())); |
|||
var expectedCustomerId = new CustomerId(UUID.randomUUID()); |
|||
user.setCustomerId(expectedCustomerId); |
|||
|
|||
when(ctxMock.getUserService()).thenReturn(userServiceMock); |
|||
doReturn(Futures.immediateFuture(user)).when(userServiceMock).findUserByIdAsync(any(), any()); |
|||
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
|||
|
|||
// WHEN
|
|||
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, user.getId()).get(); |
|||
|
|||
// THEN
|
|||
assertEquals(expectedCustomerId, actualCustomerId); |
|||
} |
|||
|
|||
@Test |
|||
public void givenAssetEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException { |
|||
// GIVEN
|
|||
var asset = new Asset(new AssetId(UUID.randomUUID())); |
|||
var expectedCustomerId = new CustomerId(UUID.randomUUID()); |
|||
asset.setCustomerId(expectedCustomerId); |
|||
|
|||
when(ctxMock.getAssetService()).thenReturn(assetServiceMock); |
|||
doReturn(Futures.immediateFuture(asset)).when(assetServiceMock).findAssetByIdAsync(any(), any()); |
|||
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
|||
|
|||
// WHEN
|
|||
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, asset.getId()).get(); |
|||
|
|||
// THEN
|
|||
assertEquals(expectedCustomerId, actualCustomerId); |
|||
} |
|||
|
|||
@Test |
|||
public void givenDeviceEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException { |
|||
// GIVEN
|
|||
var device = new Device(new DeviceId(UUID.randomUUID())); |
|||
var expectedCustomerId = new CustomerId(UUID.randomUUID()); |
|||
device.setCustomerId(expectedCustomerId); |
|||
|
|||
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); |
|||
doReturn(device).when(deviceServiceMock).findDeviceById(any(), any()); |
|||
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
|||
|
|||
// WHEN
|
|||
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, device.getId()).get(); |
|||
|
|||
// THEN
|
|||
assertEquals(expectedCustomerId, actualCustomerId); |
|||
} |
|||
|
|||
@Test |
|||
public void givenUnsupportedEntityTypes_whenFindEntityIdAsync_thenException() { |
|||
for (var entityType : EntityType.values()) { |
|||
if (!SUPPORTED_ENTITY_TYPES.contains(entityType)) { |
|||
var entityId = EntityIdFactory.getByTypeAndUuid(entityType, UUID.randomUUID()); |
|||
|
|||
var expectedExceptionMsg = "org.thingsboard.rule.engine.api.TbNodeException: Unexpected originator EntityType: " + entityType; |
|||
|
|||
var exception = assertThrows(ExecutionException.class, |
|||
() -> EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, entityId).get()); |
|||
|
|||
assertInstanceOf(TbNodeException.class, exception.getCause()); |
|||
assertEquals(expectedExceptionMsg, exception.getMessage()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
File diff suppressed because one or more lines are too long
@ -0,0 +1,38 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Injectable } from '@angular/core'; |
|||
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { HttpClient } from '@angular/common/http'; |
|||
import { catchError, map } from 'rxjs/operators'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class GitHubService { |
|||
|
|||
constructor( |
|||
private http: HttpClient |
|||
) { } |
|||
|
|||
public getGitHubStar(config?: RequestConfig): Observable<number> { |
|||
return this.http.get<any>('https://api.github.com/repos/thingsboard/thingsboard', defaultHttpOptionsFromConfig(config)).pipe( |
|||
catchError(() => of({})), |
|||
map((res: any) => res?.stargazers_count ?? 0) |
|||
) |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2025 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. |
|||
|
|||
--> |
|||
<form style="width: 800px"> |
|||
<mat-toolbar class="justify-between" color="primary"> |
|||
<h2>{{ 'alarm-rule.edit-alarm-rule-additional-info' | translate }}</h2> |
|||
<button mat-icon-button (click)="cancel()" type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<div mat-dialog-content> |
|||
<div class="flex flex-1 flex-col"> |
|||
<mat-form-field class="mat-block" appearance="outline"> |
|||
<textarea matInput [formControl]="alarmDetailsControl" rows="5" |
|||
placeholder="{{ 'alarm-rule.alarm-rule-additional-info-placeholder' | translate }}"></textarea> |
|||
<mat-hint [innerHTML]="'alarm-rule.alarm-rule-additional-info-hint' | translate | safe: 'html'"></mat-hint> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div mat-dialog-actions class="justify-end"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
(click)="cancel()" cdkFocusInitial> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
@if (!data.readonly) { |
|||
<button mat-raised-button color="primary" |
|||
type="button" |
|||
(click)="save()" |
|||
[disabled]="alarmDetailsControl.invalid || !alarmDetailsControl.dirty"> |
|||
{{ 'action.save' | translate }} |
|||
</button> |
|||
} |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,60 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { FormBuilder, FormControl } from '@angular/forms'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
|
|||
export interface AlarmRuleDetailsDialogData { |
|||
alarmDetails: string; |
|||
readonly: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-edit-alarm-details-dialog', |
|||
templateUrl: './alarm-rule-details-dialog.component.html', |
|||
providers: [], |
|||
styleUrls: ['./cf-alarm-rules-dialog.component.scss'], |
|||
}) |
|||
export class AlarmRuleDetailsDialogComponent extends DialogComponent<AlarmRuleDetailsDialogComponent, string> { |
|||
|
|||
alarmDetailsControl: FormControl<string>; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleDetailsDialogData, |
|||
public dialogRef: MatDialogRef<AlarmRuleDetailsDialogComponent, string>, |
|||
private fb: FormBuilder) { |
|||
super(store, router, dialogRef); |
|||
|
|||
this.alarmDetailsControl = this.fb.control(this.data.alarmDetails); |
|||
if (this.data.readonly) { |
|||
this.alarmDetailsControl.disable(); |
|||
} |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
save(): void { |
|||
this.dialogRef.close(this.alarmDetailsControl.value); |
|||
} |
|||
} |
|||
@ -0,0 +1,164 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2025 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div [formGroup]="fieldFormGroup" class="calculated-field-dialog-container flex size-full max-w-4xl flex-col"> |
|||
<mat-toolbar color="primary"> |
|||
<h2>{{ 'alarm-rule.alarm-rule' | translate}}</h2> |
|||
<span class="flex-1"></span> |
|||
<div tb-help="alarmRule"></div> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<div mat-dialog-content class="flex-1"> |
|||
<div class="tb-form-panel no-border no-padding"> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title">{{ 'common.general' | translate }}</div> |
|||
<div class="flex items-center gap-2"> |
|||
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label>{{ 'alarm-rule.alarm-type' | translate }}</mat-label> |
|||
<input matInput maxlength="255" formControlName="name" required> |
|||
@if (fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched) { |
|||
<mat-error> |
|||
@if (fieldFormGroup.get('name').hasError('required')) { |
|||
{{ 'alarm-rule.alarm-type-required' | translate }} |
|||
} @else if (fieldFormGroup.get('name').hasError('pattern')) { |
|||
{{ 'alarm-rule.alarm-type-pattern' | translate }} |
|||
} @else if (fieldFormGroup.get('name').hasError('maxlength')) { |
|||
{{ 'alarm-rule.alarm-type-max-length' | translate }} |
|||
} |
|||
</mat-error> |
|||
} |
|||
</mat-form-field> |
|||
<tb-entity-debug-settings-button |
|||
formControlName="debugSettings" |
|||
[class.mb-5]="fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched" |
|||
[entityType]="EntityType.CALCULATED_FIELD" |
|||
[additionalActionConfig]="additionalDebugActionConfig" |
|||
/> |
|||
</div> |
|||
</div> |
|||
<ng-container formGroupName="configuration"> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title tb-required">{{ 'calculated-fields.arguments' | translate }}</div> |
|||
<tb-calculated-field-arguments-table formControlName="arguments" |
|||
[entityId]="data.entityId" |
|||
[tenantId]="data.tenantId" |
|||
[ownerId]="data.ownerId" |
|||
[entityName]="data.entityName"/> |
|||
</div> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title">{{ 'alarm-rule.create-alarm-rules' | translate }}</div> |
|||
<div class="flex flex-1 flex-col"> |
|||
<tb-create-cf-alarm-rules formControlName="createRules" [arguments]="arguments"> |
|||
</tb-create-cf-alarm-rules> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title">{{ 'alarm-rule.clear-alarm-rule' | translate }}</div> |
|||
<div class="flex flex-row items-center justify-start gap-2 pb-2" |
|||
[class.!hidden]="!configFormGroup.get('clearRule').value"> |
|||
<div class="clear-alarm-rule flex flex-1 flex-row"> |
|||
<tb-cf-alarm-rule formControlName="clearRule" class="flex-1" [arguments]="arguments"> |
|||
</tb-cf-alarm-rule> |
|||
</div> |
|||
<button mat-icon-button |
|||
class="button-icon" |
|||
type="button" |
|||
(click)="removeClearAlarmRule()" |
|||
matTooltip="{{ 'action.remove' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
</div> |
|||
<div *ngIf="!configFormGroup.get('clearRule').value"> |
|||
<span translate style="margin: 16px 0" |
|||
class="tb-prompt flex items-center justify-center">alarm-rule.no-clear-alarm-rule</span> |
|||
</div> |
|||
<div [class.!hidden]="configFormGroup.get('clearRule').value"> |
|||
<button mat-stroked-button color="primary" |
|||
type="button" |
|||
(click)="addClearAlarmRule()" |
|||
matTooltip="{{ 'alarm-rule.add-clear-alarm-rule' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon class="button-icon">add_circle_outline</mat-icon> |
|||
{{ 'alarm-rule.add-clear-alarm-rule' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-panel no-gap"> |
|||
<mat-expansion-panel class="tb-settings" [expanded]="false"> |
|||
<mat-expansion-panel-header> |
|||
{{ 'alarm-rule.advanced-settings' | translate }} |
|||
</mat-expansion-panel-header> |
|||
<ng-template matExpansionPanelContent> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide margin" formControlName="propagate"> |
|||
{{ 'alarm-rule.propagate-alarm' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
@if (configFormGroup.get('propagate').value) { |
|||
<mat-form-field floatLabel="always" class="mat-block" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label translate>alarm-rule.alarm-rule-relation-types-list</mat-label> |
|||
<mat-chip-grid #relationTypesChipList> |
|||
<mat-chip-row |
|||
*ngFor="let key of configFormGroup.get('propagateRelationTypes').value;" |
|||
(removed)="removeRelationType(key)"> |
|||
{{key}} |
|||
<mat-icon matChipRemove>close</mat-icon> |
|||
</mat-chip-row> |
|||
<input matInput type="text" placeholder="{{'alarm-rule.alarm-rule-relation-types-list' | translate}}" |
|||
[matChipInputFor]="relationTypesChipList" |
|||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" |
|||
matChipInputAddOnBlur |
|||
(matChipInputTokenEnd)="addRelationType($event)"> |
|||
</mat-chip-grid> |
|||
<mat-hint innerHTML="{{ 'alarm-rule.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint> |
|||
</mat-form-field> |
|||
} |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide margin" formControlName="propagateToOwner"> |
|||
{{ 'alarm-rule.propagate-alarm-to-owner' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide margin" formControlName="propagateToTenant"> |
|||
{{ 'alarm-rule.propagate-alarm-to-tenant' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</ng-template> |
|||
</mat-expansion-panel> |
|||
</div> |
|||
</ng-container> |
|||
</div> |
|||
</div> |
|||
<div mat-dialog-actions class="justify-end"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
cdkFocusInitial |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
(click)="add()" |
|||
[disabled]="(isLoading$ | async) || fieldFormGroup.invalid || !fieldFormGroup.dirty"> |
|||
{{ data.buttonTitle | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,34 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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. |
|||
*/ |
|||
|
|||
.calculated-field-dialog-container { |
|||
width: 869px; |
|||
max-width: 100%; |
|||
} |
|||
|
|||
.clear-alarm-rule { |
|||
border: 1px solid rgba(0, 0, 0, 0.12); |
|||
border-left-width: 4px; |
|||
border-left-color: green; |
|||
border-radius: 4px; |
|||
padding: 8px; |
|||
min-width: 0; |
|||
} |
|||
|
|||
.button-icon { |
|||
color: rgba(0, 0, 0, 0.38); |
|||
min-width: 40px; |
|||
} |
|||
@ -0,0 +1,179 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 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, DestroyRef, Inject, ViewEncapsulation } from '@angular/core'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { CalculatedField, CalculatedFieldArgument, CalculatedFieldType } from '@shared/models/calculated-field.models'; |
|||
import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { ScriptLanguage } from '@shared/models/rule-node.models'; |
|||
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; |
|||
import { COMMA, ENTER, SEMICOLON } from "@angular/cdk/keycodes"; |
|||
import { MatChipInputEvent } from "@angular/material/chips"; |
|||
import { AlarmRule, AlarmRuleConditionType, AlarmRuleExpressionType } from "@shared/models/alarm-rule.models"; |
|||
import { deepTrim } from "@core/utils"; |
|||
|
|||
export interface AlarmRuleDialogData { |
|||
value?: CalculatedField; |
|||
buttonTitle: string; |
|||
entityId: EntityId; |
|||
tenantId: string; |
|||
entityName?: string; |
|||
ownerId: EntityId; |
|||
additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; |
|||
isDirty?: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-alarm-rule-dialog', |
|||
templateUrl: './alarm-rule-dialog.component.html', |
|||
styleUrls: ['./alarm-rule-dialog.component.scss'], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogComponent, CalculatedField> { |
|||
|
|||
fieldFormGroup = this.fb.group({ |
|||
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], |
|||
type: [CalculatedFieldType.ALARM], |
|||
debugSettings: [], |
|||
configuration: this.fb.group({ |
|||
arguments: this.fb.control({}), |
|||
propagate: [false], |
|||
propagateToOwner: [false], |
|||
propagateToTenant: [false], |
|||
propagateRelationTypes: [null], |
|||
createRules: [null], |
|||
clearRule: [null], |
|||
}), |
|||
}); |
|||
|
|||
additionalDebugActionConfig = this.data.value?.id ? { |
|||
...this.data.additionalDebugActionConfig, |
|||
action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }), |
|||
} : null; |
|||
|
|||
readonly EntityType = EntityType; |
|||
readonly CalculatedFieldType = CalculatedFieldType; |
|||
readonly ScriptLanguage = ScriptLanguage; |
|||
|
|||
separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData, |
|||
protected dialogRef: MatDialogRef<AlarmRuleDialogComponent, CalculatedField>, |
|||
private calculatedFieldsService: CalculatedFieldsService, |
|||
private destroyRef: DestroyRef, |
|||
private fb: FormBuilder) { |
|||
super(store, router, dialogRef); |
|||
this.observeIsLoading(); |
|||
this.applyDialogData(); |
|||
} |
|||
|
|||
get configFormGroup(): FormGroup { |
|||
return this.fieldFormGroup.get('configuration') as FormGroup; |
|||
} |
|||
|
|||
get arguments(): Record<string, CalculatedFieldArgument> { |
|||
return this.fieldFormGroup.get('configuration.arguments').value; |
|||
} |
|||
|
|||
public removeClearAlarmRule() { |
|||
this.configFormGroup.patchValue({clearRule: null}); |
|||
this.fieldFormGroup.markAsDirty(); |
|||
} |
|||
|
|||
public addClearAlarmRule() { |
|||
const clearAlarmRule: AlarmRule = { |
|||
condition: { |
|||
type: AlarmRuleConditionType.SIMPLE, |
|||
expression: { |
|||
type: AlarmRuleExpressionType.SIMPLE |
|||
} |
|||
} |
|||
}; |
|||
this.configFormGroup.patchValue({clearRule: clearAlarmRule}); |
|||
} |
|||
|
|||
removeRelationType(key: string): void { |
|||
const keys: string[] = this.configFormGroup.get('propagateRelationTypes').value; |
|||
const index = keys.indexOf(key); |
|||
if (index >= 0) { |
|||
keys.splice(index, 1); |
|||
this.configFormGroup.get('propagateRelationTypes').setValue(keys); |
|||
} |
|||
} |
|||
|
|||
addRelationType(event: MatChipInputEvent): void { |
|||
const input = event.chipInput.inputElement; |
|||
let value = (event.value ?? '').trim(); |
|||
if (value) { |
|||
let keys: string[] = this.configFormGroup.get('propagateRelationTypes').value ?? []; |
|||
if (keys.indexOf(value) === -1) { |
|||
keys.push(value); |
|||
this.configFormGroup.get('propagateRelationTypes').setValue(keys); |
|||
} |
|||
} |
|||
if (input) { |
|||
input.value = ''; |
|||
} |
|||
} |
|||
|
|||
get fromGroupValue(): CalculatedField { |
|||
return deepTrim(this.fieldFormGroup.value as CalculatedField); |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
add(): void { |
|||
if (this.fieldFormGroup.valid) { |
|||
const alarmRule = { entityId: this.data.entityId, ...(this.data.value ?? {}), ...this.fromGroupValue}; |
|||
alarmRule.configuration.type = CalculatedFieldType.ALARM; |
|||
|
|||
this.calculatedFieldsService.saveCalculatedField(alarmRule) |
|||
.pipe(takeUntilDestroyed(this.destroyRef)) |
|||
.subscribe(calculatedField => this.dialogRef.close(calculatedField)); |
|||
} |
|||
} |
|||
|
|||
private applyDialogData(): void { |
|||
const { configuration = {}, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {}; |
|||
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false}); |
|||
} |
|||
|
|||
private observeIsLoading(): void { |
|||
this.isLoading$.pipe(takeUntilDestroyed()).subscribe(loading => { |
|||
if (loading) { |
|||
this.fieldFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.fieldFormGroup.enable({emitEvent: false}); |
|||
if (this.data.isDirty) { |
|||
this.fieldFormGroup.markAsDirty(); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 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 { NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { AlarmRuleDialogComponent } from "@home/components/alarm-rules/alarm-rule-dialog.component"; |
|||
import { CreateCfAlarmRulesComponent } from "@home/components/alarm-rules/create-cf-alarm-rules.component"; |
|||
import { CfAlarmRuleComponent } from "@home/components/alarm-rules/cf-alarm-rule.component"; |
|||
import { CfAlarmRuleConditionComponent } from "@home/components/alarm-rules/cf-alarm-rule-condition.component"; |
|||
import { |
|||
CfAlarmRuleConditionDialogComponent |
|||
} from "@home/components/alarm-rules/cf-alarm-rule-condition-dialog.component"; |
|||
import { CfAlarmScheduleComponent } from "@home/components/alarm-rules/cf-alarm-schedule.component"; |
|||
import { CfAlarmScheduleDialogComponent } from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component"; |
|||
import { |
|||
EntityDebugSettingsButtonComponent |
|||
} from "@home/components/entity/debug/entity-debug-settings-button.component"; |
|||
import { AlarmRuleFilterTextComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-text.component"; |
|||
import { |
|||
CalculatedFieldArgumentsTableModule |
|||
} from "@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module"; |
|||
import { |
|||
AlarmRuleFilterPredicateListComponent |
|||
} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component"; |
|||
import { |
|||
AlarmRuleFilterPredicateComponent |
|||
} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate.component"; |
|||
import { |
|||
AlarmRuleFilterPredicateValueComponent |
|||
} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component"; |
|||
import { |
|||
AlarmRuleComplexFilterPredicateDialogComponent |
|||
} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component"; |
|||
import { AlarmRuleFilterListComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-list.component"; |
|||
import { AlarmRuleFilterDialogComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component"; |
|||
import { AlarmRuleDetailsDialogComponent } from "@home/components/alarm-rules/alarm-rule-details-dialog.component"; |
|||
|
|||
@NgModule({ |
|||
declarations: [ |
|||
AlarmRuleDialogComponent, |
|||
CreateCfAlarmRulesComponent, |
|||
CfAlarmRuleComponent, |
|||
CfAlarmRuleConditionComponent, |
|||
CfAlarmRuleConditionDialogComponent, |
|||
CfAlarmScheduleComponent, |
|||
CfAlarmScheduleDialogComponent, |
|||
AlarmRuleFilterTextComponent, |
|||
AlarmRuleFilterListComponent, |
|||
AlarmRuleFilterDialogComponent, |
|||
AlarmRuleFilterPredicateListComponent, |
|||
AlarmRuleFilterPredicateComponent, |
|||
AlarmRuleFilterPredicateValueComponent, |
|||
AlarmRuleComplexFilterPredicateDialogComponent, |
|||
AlarmRuleDetailsDialogComponent, |
|||
], |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
EntityDebugSettingsButtonComponent, |
|||
CalculatedFieldArgumentsTableModule |
|||
], |
|||
exports: [ |
|||
AlarmRuleDialogComponent, |
|||
] |
|||
}) |
|||
export class AlarmRuleModule { } |
|||
@ -0,0 +1,278 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 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 { |
|||
checkBoxCell, |
|||
DateEntityTableColumn, |
|||
EntityTableColumn, |
|||
EntityTableConfig |
|||
} from '@home/models/entity/entities-table-config.models'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { Direction } from '@shared/models/page/sort-order'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { PageData } from '@shared/models/page/page-data'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
|||
import { DestroyRef, Renderer2 } from '@angular/core'; |
|||
import { EntityDebugSettings } from '@shared/models/entity.models'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; |
|||
import { catchError, filter, switchMap } from 'rxjs/operators'; |
|||
import { |
|||
ArgumentEntityType, |
|||
CalculatedField, |
|||
CalculatedFieldAlarmRule, |
|||
CalculatedFieldType, |
|||
} from '@shared/models/calculated-field.models'; |
|||
import { ImportExportService } from '@shared/import-export/import-export.service'; |
|||
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; |
|||
import { DatePipe } from '@angular/common'; |
|||
import { |
|||
AlarmRuleDialogComponent, |
|||
AlarmRuleDialogData |
|||
} from "@home/components/alarm-rules/alarm-rule-dialog.component"; |
|||
import { |
|||
CalculatedFieldDebugDialogComponent, |
|||
CalculatedFieldDebugDialogData |
|||
} from "@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component"; |
|||
import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.models"; |
|||
|
|||
export class AlarmRulesTableConfig extends EntityTableConfig<any> { |
|||
|
|||
readonly tenantId = getCurrentAuthUser(this.store).tenantId; |
|||
additionalDebugActionConfig = { |
|||
title: this.translate.instant('calculated-fields.see-debug-events'), |
|||
action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField), |
|||
}; |
|||
|
|||
constructor(private calculatedFieldsService: CalculatedFieldsService, |
|||
private translate: TranslateService, |
|||
private dialog: MatDialog, |
|||
private datePipe: DatePipe, |
|||
public entityId: EntityId = null, |
|||
private store: Store<AppState>, |
|||
private destroyRef: DestroyRef, |
|||
private renderer: Renderer2, |
|||
public entityName: string, |
|||
private ownerId: EntityId = null, |
|||
private importExportService: ImportExportService, |
|||
private entityDebugSettingsService: EntityDebugSettingsService, |
|||
) { |
|||
super(); |
|||
this.tableTitle = this.translate.instant('alarm-rule.alarm-rules'); |
|||
this.detailsPanelEnabled = false; |
|||
this.pageMode = false; |
|||
this.entityType = EntityType.CALCULATED_FIELD; |
|||
this.entityTranslations = { |
|||
type: 'alarm-rule.alarm-rule', |
|||
typePlural: 'alarm-rule.alarm-rules', |
|||
list: 'alarm-rule.list', |
|||
add: 'action.add', |
|||
noEntities: 'alarm-rule.no-found', |
|||
search: 'action.search', |
|||
selectedEntities: 'alarm-rule.selected-fields' |
|||
}; |
|||
|
|||
this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink); |
|||
this.addEntity = this.getCalculatedAlarmDialog.bind(this); |
|||
this.deleteEntityTitle = (field: CalculatedField) => this.translate.instant('alarm-rule.delete-title', {title: field.name}); |
|||
this.deleteEntityContent = () => this.translate.instant('alarm-rule.delete-text'); |
|||
this.deleteEntitiesTitle = count => this.translate.instant('alarm-rule.delete-multiple-title', {count}); |
|||
this.deleteEntitiesContent = () => this.translate.instant('alarm-rule.delete-multiple-text'); |
|||
this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id); |
|||
this.addActionDescriptors = [ |
|||
{ |
|||
name: this.translate.instant('alarm-rule.create'), |
|||
icon: 'insert_drive_file', |
|||
isEnabled: () => true, |
|||
onAction: ($event) => this.getTable().addEntity($event) |
|||
}, |
|||
{ |
|||
name: this.translate.instant('alarm-rule.import'), |
|||
icon: 'file_upload', |
|||
isEnabled: () => true, |
|||
onAction: () => this.importCalculatedField() |
|||
} |
|||
]; |
|||
|
|||
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; |
|||
this.columns.push(new DateEntityTableColumn<CalculatedFieldAlarmRule>('createdTime', 'common.created-time', this.datePipe, '150px')); |
|||
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('name', 'alarm-rule.alarm-type', '33%')); |
|||
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('createRule', 'alarm-rule.severities', '67%', |
|||
entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '), |
|||
() => ({}), false)); |
|||
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('clearRule', 'alarm-rule.cleared', '60px', |
|||
entity => checkBoxCell(!!entity.configuration.clearRule), ()=> { return {padding: '0 14px'}}, false)); |
|||
|
|||
this.cellActionDescriptors.push( |
|||
{ |
|||
name: this.translate.instant('action.export'), |
|||
icon: 'file_download', |
|||
isEnabled: () => true, |
|||
onAction: (event$, entity) => this.exportAlarmRule(event$, entity), |
|||
}, |
|||
{ |
|||
name: this.translate.instant('entity-view.events'), |
|||
icon: 'mdi:clipboard-text-clock', |
|||
isEnabled: () => true, |
|||
onAction: (_, entity) => this.openDebugEventsDialog(entity), |
|||
}, |
|||
{ |
|||
name: '', |
|||
nameFunction: entity => this.entityDebugSettingsService.getDebugConfigLabel(entity?.debugSettings), |
|||
icon: 'mdi:bug', |
|||
isEnabled: () => true, |
|||
iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', |
|||
onAction: ($event, entity) => this.onOpenDebugConfig($event, entity), |
|||
}, |
|||
{ |
|||
name: this.translate.instant('action.edit'), |
|||
icon: 'edit', |
|||
isEnabled: () => true, |
|||
onAction: (_, entity) => this.editCalculatedField(entity), |
|||
} |
|||
); |
|||
} |
|||
|
|||
fetchCalculatedFields(pageLink: PageLink): Observable<PageData<CalculatedField>> { |
|||
return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink, CalculatedFieldType.ALARM); |
|||
} |
|||
|
|||
onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void { |
|||
const { debugSettings = {}, id } = calculatedField; |
|||
const additionalActionConfig = { |
|||
...this.additionalDebugActionConfig, |
|||
action: () => this.openDebugEventsDialog(calculatedField) |
|||
}; |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
|
|||
const { viewContainerRef, renderer } = this.entityDebugSettingsService; |
|||
if (!viewContainerRef || !renderer) { |
|||
this.entityDebugSettingsService.viewContainerRef = this.getTable().viewContainerRef; |
|||
this.entityDebugSettingsService.renderer = this.renderer; |
|||
} |
|||
|
|||
this.entityDebugSettingsService.openDebugStrategyPanel({ |
|||
debugSettings, |
|||
debugConfig: { |
|||
entityType: EntityType.CALCULATED_FIELD, |
|||
entityLabel: 'alarm-rule.alarm-rule', |
|||
additionalActionConfig, |
|||
}, |
|||
onSettingsAppliedFn: settings => this.onDebugConfigChanged(id.id, settings) |
|||
}, $event.target as Element); |
|||
} |
|||
|
|||
private editCalculatedField(calculatedField: CalculatedField, isDirty = false): void { |
|||
this.getCalculatedAlarmDialog(calculatedField, 'action.apply', isDirty) |
|||
.subscribe((res) => { |
|||
if (res) { |
|||
this.updateData(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private getCalculatedAlarmDialog(value?: CalculatedField, buttonTitle = 'action.add', isDirty = false): Observable<CalculatedField> { |
|||
return this.dialog.open<AlarmRuleDialogComponent, AlarmRuleDialogData, CalculatedField>(AlarmRuleDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
value, |
|||
buttonTitle, |
|||
entityId: this.entityId, |
|||
tenantId: this.tenantId, |
|||
entityName: this.entityName, |
|||
ownerId: this.ownerId, |
|||
additionalDebugActionConfig: this.additionalDebugActionConfig, |
|||
isDirty, |
|||
}, |
|||
enterAnimationDuration: isDirty ? 0 : null, |
|||
}) |
|||
.afterClosed() |
|||
.pipe(filter(Boolean)); |
|||
} |
|||
|
|||
private openDebugEventsDialog(calculatedField: CalculatedField): void { |
|||
this.dialog.open<CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, null>(CalculatedFieldDebugDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
tenantId: this.tenantId, |
|||
value: calculatedField, |
|||
getTestScriptDialogFn: null, |
|||
} |
|||
}) |
|||
.afterClosed() |
|||
.subscribe(); |
|||
} |
|||
|
|||
private exportAlarmRule($event: Event, calculatedField: CalculatedField): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.importExportService.exportCalculatedField(calculatedField.id.id); |
|||
} |
|||
|
|||
private importCalculatedField(): void { |
|||
this.importExportService.openCalculatedFieldImportDialog() |
|||
.pipe( |
|||
filter(Boolean), |
|||
switchMap(calculatedField => this.getCalculatedAlarmDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)), |
|||
filter(Boolean), |
|||
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)), |
|||
filter(Boolean), |
|||
takeUntilDestroyed(this.destroyRef) |
|||
) |
|||
.subscribe(() => this.updateData()); |
|||
} |
|||
|
|||
private updateImportedCalculatedField(calculatedField: CalculatedField): CalculatedField { |
|||
if (calculatedField.type === CalculatedFieldType.GEOFENCING) { |
|||
calculatedField.configuration.zoneGroups = Object.keys(calculatedField.configuration.zoneGroups).reduce((acc, key) => { |
|||
const arg = calculatedField.configuration.zoneGroups[key]; |
|||
acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant |
|||
? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } } |
|||
: arg; |
|||
return acc; |
|||
}, {}); |
|||
} else { |
|||
calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { |
|||
const arg = calculatedField.configuration.arguments[key]; |
|||
acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant |
|||
? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } } |
|||
: arg; |
|||
return acc; |
|||
}, {}); |
|||
} |
|||
|
|||
return calculatedField; |
|||
} |
|||
|
|||
private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void { |
|||
this.calculatedFieldsService.getCalculatedFieldById(id).pipe( |
|||
switchMap(field => this.calculatedFieldsService.saveCalculatedField({ ...field, debugSettings })), |
|||
catchError(() => of(null)), |
|||
takeUntilDestroyed(this.destroyRef), |
|||
).subscribe(() => this.updateData()); |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2025 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. |
|||
|
|||
--> |
|||
@if (alarmRulesTableConfig) { |
|||
<tb-entities-table [entitiesTableConfig]="alarmRulesTableConfig"></tb-entities-table> |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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 ::ng-deep { |
|||
tb-entities-table { |
|||
--mat-sidenav-content-background-color: white; |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 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 { |
|||
ChangeDetectionStrategy, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
DestroyRef, |
|||
effect, |
|||
input, |
|||
Renderer2, |
|||
ViewChild, |
|||
} from '@angular/core'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; |
|||
import { ImportExportService } from '@shared/import-export/import-export.service'; |
|||
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; |
|||
import { DatePipe } from '@angular/common'; |
|||
import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config"; |
|||
|
|||
@Component({ |
|||
selector: 'tb-alarm-rules-table', |
|||
templateUrl: './alarm-rules-table.component.html', |
|||
styleUrls: ['./alarm-rules-table.component.scss'], |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
providers: [EntityDebugSettingsService] |
|||
}) |
|||
export class AlarmRulesTableComponent { |
|||
|
|||
@ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent; |
|||
|
|||
active = input<boolean>(); |
|||
entityId = input<EntityId>(); |
|||
entityName = input<string>(); |
|||
ownerId = input<EntityId>(); |
|||
|
|||
alarmRulesTableConfig: AlarmRulesTableConfig; |
|||
|
|||
constructor(private calculatedFieldsService: CalculatedFieldsService, |
|||
private translate: TranslateService, |
|||
private dialog: MatDialog, |
|||
private store: Store<AppState>, |
|||
private datePipe: DatePipe, |
|||
private cd: ChangeDetectorRef, |
|||
private renderer: Renderer2, |
|||
private importExportService: ImportExportService, |
|||
private entityDebugSettingsService: EntityDebugSettingsService, |
|||
private destroyRef: DestroyRef) { |
|||
|
|||
effect(() => { |
|||
if (this.active()) { |
|||
this.alarmRulesTableConfig = new AlarmRulesTableConfig( |
|||
this.calculatedFieldsService, |
|||
this.translate, |
|||
this.dialog, |
|||
this.datePipe, |
|||
this.entityId(), |
|||
this.store, |
|||
this.destroyRef, |
|||
this.renderer, |
|||
this.entityName(), |
|||
this.ownerId(), |
|||
this.importExportService, |
|||
this.entityDebugSettingsService, |
|||
); |
|||
this.cd.markForCheck(); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,188 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2025 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. |
|||
|
|||
--> |
|||
<form [formGroup]="conditionFormGroup" (ngSubmit)="save()"> |
|||
<mat-toolbar color="primary"> |
|||
<h2>{{ (readonly ? 'alarm-rule.alarm-rule-condition' : 'alarm-rule.edit-alarm-rule-condition') | translate }}</h2> |
|||
<span class="flex-1"></span> |
|||
<div formGroupName="expression" class="flex flex-row items-center justify-start gt-xs:gap-4" style="min-width: fit-content;"> |
|||
<tb-toggle-select formControlName="type" appearance="fill-invert" selectMediaBreakpoint="xs"> |
|||
<tb-toggle-option [value]="AlarmRuleExpressionType.SIMPLE">{{ 'alarm-rule.expression-type.simple' | translate }}</tb-toggle-option> |
|||
<tb-toggle-option [value]="AlarmRuleExpressionType.TBEL">{{ 'alarm-rule.expression-type.tbel' | translate }}</tb-toggle-option> |
|||
</tb-toggle-select> |
|||
</div> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<div mat-dialog-content> |
|||
<div class="tb-form-panel no-padding no-border"> |
|||
<div class="flex flex-1 flex-col" formGroupName="expression"> |
|||
@if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) { |
|||
<div class="tb-form-panel"> |
|||
<div class="flex flex-row items-center justify-between"> |
|||
<div class="tb-form-panel-title">{{ 'alarm-rule.argument-filters' | translate }}</div> |
|||
<tb-toggle-select formControlName="operation" |
|||
selectMediaBreakpoint="xs"> |
|||
<tb-toggle-option [value]="ComplexOperation.AND">{{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }}</tb-toggle-option> |
|||
<tb-toggle-option [value]="ComplexOperation.OR">{{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate }}</tb-toggle-option> |
|||
</tb-toggle-select> |
|||
</div> |
|||
<tb-alarm-rule-filter-list |
|||
[operation]="conditionFormGroup.get('expression.operation').value" |
|||
[arguments]="arguments" |
|||
formControlName="filters"> |
|||
</tb-alarm-rule-filter-list> |
|||
</div> |
|||
} @else { |
|||
<div class="tb-form-panel no-gap"> |
|||
<div class="tb-form-panel-title tb-required"> |
|||
{{ 'alarm-rule.script' | translate }} |
|||
</div> |
|||
<tb-js-func formControlName="expression" |
|||
functionName="expression" |
|||
[disableUndefinedCheck]="true" |
|||
[scriptLanguage]="scriptLanguage.TBEL" |
|||
[functionArgs]="functionArgs" |
|||
[highlightRules]="argumentsHighlightRules" |
|||
[editorCompleter]="argumentsEditorCompleter" |
|||
noValidate="true"> |
|||
<div toolbarPrefixButton |
|||
class="tb-primary-background tbel-script-lang-chip">{{ 'alarm-rule.expression-type.tbel' | translate }} |
|||
</div> |
|||
</tb-js-func> |
|||
</div> |
|||
} |
|||
</div> |
|||
<section class="tb-form-panel"> |
|||
<div class="tb-form-panel-title">{{ 'alarm-rule.condition-settings' | translate }}</div> |
|||
<mat-form-field class="mat-block" hideRequiredMarker appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label translate>alarm-rule.condition-type</mat-label> |
|||
<mat-select formControlName="type" required> |
|||
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType"> |
|||
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
@if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) { |
|||
<div class="tb-form-panel stroked no-padding-bottom"> |
|||
<div class="flex flex-row items-center justify-between"> |
|||
<div class="tb-form-panel-title">{{ 'alarm-rule.value' | translate }}</div> |
|||
<tb-toggle-select [formControl]="durationDynamicModeControl" |
|||
selectMediaBreakpoint="xs"> |
|||
<tb-toggle-option [value]="false">{{ 'alarm-rule.static' | translate }}</tb-toggle-option> |
|||
<tb-toggle-option [value]="true">{{ 'alarm-rule.dynamic' | translate }}</tb-toggle-option> |
|||
</tb-toggle-select> |
|||
</div> |
|||
<div class="flex flex-row gap-2 xs:flex-col"> |
|||
<div class="flex-1" [class.!hidden]="durationDynamicModeControl.value"> |
|||
<ng-container *ngTemplateOutlet="staticValueTemplate; context:{type: AlarmConditionType.DURATION, groupName: 'value'}"></ng-container> |
|||
</div> |
|||
<div class="flex-1" [class.!hidden]="!durationDynamicModeControl.value"> |
|||
<ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{groupName: 'value'}"></ng-container> |
|||
</div> |
|||
<div class="max-w-23% flex-full xs:max-w-full"> |
|||
<mat-form-field class="mat-block" appearance="outline"> |
|||
<mat-select formControlName="unit" required |
|||
placeholder="{{ 'alarm-rule.condition-duration-time-unit' | translate }}"> |
|||
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit"> |
|||
{{ timeUnitTranslations.get(timeUnit) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
<mat-error *ngIf="conditionFormGroup.get('unit').hasError('required')"> |
|||
{{ 'alarm-rule.condition-duration-time-unit-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
} @else if (conditionFormGroup.get('type').value == AlarmConditionType.REPEATING) { |
|||
<div class="tb-form-panel stroked"> |
|||
<div class="flex flex-row items-center justify-between"> |
|||
<div class="tb-form-panel-title">{{ 'alarm-rule.value' | translate }}</div> |
|||
<tb-toggle-select [formControl]="repeatingDynamicModeControl" selectMediaBreakpoint="xs"> |
|||
<tb-toggle-option [value]="false">{{ 'alarm-rule.static' | translate }}</tb-toggle-option> |
|||
<tb-toggle-option [value]="true">{{ 'alarm-rule.dynamic' | translate }}</tb-toggle-option> |
|||
</tb-toggle-select> |
|||
</div> |
|||
<div class="flex flex-1 flex-row gap-2"> |
|||
<div class="flex-1" [class.!hidden]="repeatingDynamicModeControl.value"> |
|||
<ng-container *ngTemplateOutlet="staticValueTemplate; context:{type: AlarmConditionType.REPEATING, groupName: 'count'}"></ng-container> |
|||
</div> |
|||
<div class="flex-1" [class.!hidden]="!repeatingDynamicModeControl.value"> |
|||
<ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{groupName: 'count'}"></ng-container> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
} |
|||
</section> |
|||
</div> |
|||
</div> |
|||
<div mat-dialog-actions class="flex items-center justify-end"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
(click)="cancel()" cdkFocusInitial> |
|||
{{ (readonly ? 'action.close' : 'action.cancel') | translate }} |
|||
</button> |
|||
@if (!readonly) { |
|||
<button mat-raised-button color="primary" |
|||
*ngIf="!readonly" |
|||
type="submit" |
|||
[disabled]="conditionFormGroup.invalid || !conditionFormGroup.dirty"> |
|||
{{ 'action.save' | translate }} |
|||
</button> |
|||
} |
|||
</div> |
|||
|
|||
<ng-template #staticValueTemplate let-type="type" let-groupName="groupName"> |
|||
<div class="flex flex-1 flex-row items-center gap-2" [formGroupName]="groupName"> |
|||
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
|||
<input type="number" matInput |
|||
step="1" min="1" max="2147483647" |
|||
formControlName="staticValue"> |
|||
<mat-label>{{ defaultValuePlaceholder | translate }}</mat-label> |
|||
@if (conditionFormGroup.get(groupName).get('staticValue').hasError('required')) { |
|||
<mat-error>{{ defaultValueRequiredError | translate }}</mat-error> |
|||
} @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('min')) { |
|||
<mat-error>{{ defaultValueRangeError | translate }}</mat-error> |
|||
} @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('max')) { |
|||
<mat-error>{{ defaultValueRangeError | translate }}</mat-error> |
|||
} @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('pattern')) { |
|||
<mat-error>{{ defaultValuePatternError | translate }}</mat-error> |
|||
} |
|||
</mat-form-field> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template #dynamicValueTemplate let-type="type" let-groupName="groupName"> |
|||
<ng-container [formGroupName]="groupName"> |
|||
<mat-form-field class="flex flex-1" appearance="outline"> |
|||
<mat-label translate>alarm-rule.value-argument</mat-label> |
|||
<mat-select formControlName="dynamicValueArgument" placeholder="{{ 'action.set' | translate }}"> |
|||
@for (argument of argumentsList; track argument) { |
|||
<mat-option [value]="argument">{{ argument }}</mat-option> |
|||
} |
|||
</mat-select> |
|||
<mat-error *ngIf="conditionFormGroup.get(groupName).get('dynamicValueArgument').hasError('required')"> |
|||
{{ 'calculated-fields.hint.argument-name-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</ng-container> |
|||
</ng-template> |
|||
</form> |
|||
|
|||
@ -0,0 +1,226 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { FormBuilder, Validators } from '@angular/forms'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { ScriptLanguage } from "@shared/models/rule-node.models"; |
|||
import { |
|||
AlarmRuleCondition, |
|||
AlarmRuleConditionType, |
|||
AlarmRuleConditionTypeTranslationMap, |
|||
AlarmRuleExpressionType |
|||
} from "@shared/models/alarm-rule.models"; |
|||
import { |
|||
CalculatedFieldArgument, |
|||
getCalculatedFieldArgumentsEditorCompleter, |
|||
getCalculatedFieldArgumentsHighlights |
|||
} from "@shared/models/calculated-field.models"; |
|||
import { TbEditorCompleter } from "@shared/models/ace/completion.models"; |
|||
import { AceHighlightRules } from "@shared/models/ace/ace.models"; |
|||
import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models"; |
|||
|
|||
export interface CfAlarmRuleConditionDialogData { |
|||
readonly: boolean; |
|||
condition: AlarmRuleCondition; |
|||
arguments?: Record<string, CalculatedFieldArgument>; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-cf-alarm-rule-condition-dialog', |
|||
templateUrl: './cf-alarm-rule-condition-dialog.component.html', |
|||
providers: [], |
|||
styleUrls: ['./cf-alarm-rules-dialog.component.scss'], |
|||
}) |
|||
export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition> { |
|||
|
|||
AlarmRuleExpressionType = AlarmRuleExpressionType; |
|||
|
|||
timeUnits = Object.values(TimeUnit); |
|||
timeUnitTranslations = timeUnitTranslationMap; |
|||
alarmConditionTypes = Object.values(AlarmRuleConditionType); |
|||
AlarmConditionType = AlarmRuleConditionType; |
|||
alarmConditionTypeTranslation = AlarmRuleConditionTypeTranslationMap; |
|||
readonly = this.data.readonly; |
|||
condition = this.data.condition; |
|||
|
|||
conditionFormGroup = this.fb.group({ |
|||
expression: this.fb.group({ |
|||
type: [AlarmRuleExpressionType.SIMPLE], |
|||
expression: ['', [Validators.required]], |
|||
operation: [ComplexOperation.AND], |
|||
filters: [null], |
|||
}), |
|||
type: this.fb.control(AlarmRuleConditionType.SIMPLE, Validators.required), |
|||
unit: this.fb.control(TimeUnit.SECONDS, Validators.required), |
|||
value: this.fb.group({ |
|||
staticValue: this.fb.control<number | null>(null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]), |
|||
dynamicValueArgument: this.fb.control<string>('', Validators.required), |
|||
}), |
|||
count: this.fb.group({ |
|||
staticValue: this.fb.control<number | null>(null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]), |
|||
dynamicValueArgument: this.fb.control<string>('', Validators.required), |
|||
}), |
|||
}); |
|||
|
|||
readonly scriptLanguage = ScriptLanguage; |
|||
|
|||
defaultValuePlaceholder = ''; |
|||
defaultValueRequiredError = ''; |
|||
defaultValueRangeError = ''; |
|||
defaultValuePatternError = ''; |
|||
|
|||
durationDynamicModeControl = this.fb.control<boolean>(false); |
|||
repeatingDynamicModeControl = this.fb.control<boolean>(false); |
|||
|
|||
ComplexOperation = ComplexOperation; |
|||
complexOperationTranslationMap = complexOperationTranslationMap; |
|||
|
|||
functionArgs: Array<string>; |
|||
argumentsEditorCompleter: TbEditorCompleter; |
|||
argumentsHighlightRules: AceHighlightRules; |
|||
|
|||
arguments = this.data.arguments; |
|||
argumentsList: Array<string>; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData, |
|||
public dialogRef: MatDialogRef<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition>, |
|||
private fb: FormBuilder) { |
|||
super(store, router, dialogRef); |
|||
|
|||
this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)]; |
|||
this.argumentsEditorCompleter = getCalculatedFieldArgumentsEditorCompleter(this.data.arguments); |
|||
this.argumentsHighlightRules = getCalculatedFieldArgumentsHighlights(this.data.arguments); |
|||
this.argumentsList = this.arguments ? Object.keys(this.arguments): []; |
|||
|
|||
this.conditionFormGroup.patchValue({ |
|||
expression: { |
|||
type: this.condition?.expression?.type ?? AlarmRuleExpressionType.SIMPLE, |
|||
expression: this.condition?.expression?.expression ?? null, |
|||
filters: this.condition?.expression?.filters ?? [], |
|||
operation: this.condition?.expression?.operation ?? ComplexOperation.AND |
|||
}, |
|||
type: this.condition?.type ?? AlarmRuleConditionType.SIMPLE, |
|||
unit: this.condition?.unit ?? TimeUnit.SECONDS, |
|||
value: { |
|||
staticValue: this.condition?.value?.staticValue, |
|||
dynamicValueArgument: this.condition?.value?.dynamicValueArgument, |
|||
}, |
|||
count: { |
|||
staticValue: this.condition?.count?.staticValue ?? null, |
|||
dynamicValueArgument: this.condition?.count?.dynamicValueArgument |
|||
} |
|||
}, {emitEvent: false}); |
|||
|
|||
this.durationDynamicModeControl.patchValue(!!this.condition?.value?.dynamicValueArgument, {emitEvent: false}); |
|||
this.repeatingDynamicModeControl.patchValue(!!this.condition?.count?.dynamicValueArgument, {emitEvent: false}); |
|||
|
|||
this.conditionFormGroup.get('type').valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe((type) => { |
|||
this.updateValidators(type, true); |
|||
}); |
|||
|
|||
this.conditionFormGroup.get('expression.type').valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe((type) => { |
|||
this.updateExpressionTypeValidator(type); |
|||
this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE); |
|||
}); |
|||
|
|||
this.durationDynamicModeControl.valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe((mode) => { |
|||
this.updateStaticValueValidator(AlarmRuleConditionType.DURATION, mode); |
|||
}); |
|||
this.repeatingDynamicModeControl.valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe((mode) => { |
|||
this.updateStaticValueValidator(AlarmRuleConditionType.REPEATING, mode); |
|||
}); |
|||
|
|||
this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE); |
|||
this.updateExpressionTypeValidator(this.condition?.expression?.type ?? 'SIMPLE'); |
|||
} |
|||
|
|||
updateStaticValueValidator(type: AlarmRuleConditionType, dynamicValue: boolean) { |
|||
const control = this.conditionFormGroup.get(type === AlarmRuleConditionType.DURATION ? 'value' : 'count'); |
|||
if (dynamicValue) { |
|||
control.get('staticValue').disable({emitEvent: false}); |
|||
control.get('dynamicValueArgument').enable({emitEvent: false}); |
|||
} else { |
|||
control.get('staticValue').enable({emitEvent: false}); |
|||
control.get('dynamicValueArgument').disable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
updateExpressionTypeValidator(type: 'SIMPLE' | 'TBEL') { |
|||
if (type === 'SIMPLE') { |
|||
this.conditionFormGroup.get(`expression.expression`).disable({emitEvent: false}); |
|||
this.conditionFormGroup.get(`expression.filters`).enable({emitEvent: false}); |
|||
} else { |
|||
this.conditionFormGroup.get(`expression.expression`).enable({emitEvent: false}); |
|||
this.conditionFormGroup.get(`expression.filters`).disable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
private updateValidators(type: AlarmRuleConditionType, emitEvent = false) { |
|||
switch (type) { |
|||
case AlarmRuleConditionType.DURATION: |
|||
this.conditionFormGroup.get('unit').enable({emitEvent: false}); |
|||
this.conditionFormGroup.get('value').enable({emitEvent: false}); |
|||
this.conditionFormGroup.get('count').disable({emitEvent: false}); |
|||
this.updateStaticValueValidator(type, this.durationDynamicModeControl.value); |
|||
this.defaultValuePlaceholder = 'alarm-rule.condition-duration-value'; |
|||
this.defaultValueRequiredError = 'alarm-rule.condition-duration-value-required'; |
|||
this.defaultValueRangeError = 'alarm-rule.condition-duration-value-range'; |
|||
this.defaultValuePatternError = 'alarm-rule.condition-duration-value-pattern'; |
|||
break; |
|||
case AlarmRuleConditionType.REPEATING: |
|||
this.conditionFormGroup.get('count').enable({emitEvent: false}); |
|||
this.conditionFormGroup.get('value').disable({emitEvent: false}); |
|||
this.conditionFormGroup.get('unit').disable({emitEvent: false}); |
|||
this.updateStaticValueValidator(type, this.repeatingDynamicModeControl.value); |
|||
this.defaultValuePlaceholder = 'alarm-rule.condition-repeating-value'; |
|||
this.defaultValueRequiredError = 'alarm-rule.condition-repeating-value-required'; |
|||
this.defaultValueRangeError = 'alarm-rule.condition-repeating-value-range'; |
|||
this.defaultValuePatternError = 'alarm-rule.condition-repeating-value-pattern'; |
|||
break; |
|||
case AlarmRuleConditionType.SIMPLE: |
|||
this.conditionFormGroup.get('value').disable({emitEvent: false}); |
|||
this.conditionFormGroup.get('count').disable({emitEvent: false}); |
|||
this.conditionFormGroup.get('unit').disable({emitEvent: false}); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
save(): void { |
|||
this.dialogRef.close(this.conditionFormGroup.value as AlarmRuleCondition); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2025 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-alarm-rule-condition flex flex-col gap-4 min-w-0" [formGroup]="alarmRuleConditionFormGroup"> |
|||
<div class="tb-form-row column-xs"> |
|||
<div class="min-w-40 xs:min-w-fit">{{ 'alarm-rule.condition' | translate }}</div> |
|||
<button [disabled]="disabled" |
|||
type="button" |
|||
class="tb-alarm-rule-condition-button" |
|||
mat-stroked-button [color]="conditionSet() ? 'primary' : 'warn'" |
|||
(click)="openFilterDialog($event)"> |
|||
<div class="flex items-center gap-2 justify-between"> |
|||
<tb-alarm-rule-filter-text [alarmRuleExpression]="alarmRuleConditionFormGroup.get('expression').value" |
|||
[arguments]="arguments" |
|||
class="flex-1" |
|||
[nowrap]="true" |
|||
[specText]="specText" |
|||
required |
|||
addFilterPrompt="{{'alarm-rule.enter-alarm-rule-condition-prompt' | translate}}"> |
|||
</tb-alarm-rule-filter-text> |
|||
<mat-icon [color]="conditionSet() ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">{{ conditionSet() ? 'edit' : 'add' }}</mat-icon> |
|||
</div> |
|||
</button> |
|||
</div> |
|||
<div class="tb-form-row column-xs"> |
|||
<div class="min-w-40 xs:min-w-fit">{{ 'alarm-rule.schedule-title' | translate }}</div> |
|||
<button [disabled]="disabled" |
|||
type="button" |
|||
class="tb-alarm-rule-condition-button" |
|||
mat-stroked-button color="primary" |
|||
(click)="openScheduleDialog($event)"> |
|||
<div class="flex items-center gap-2"> |
|||
<span class="tb-alarm-rule-condition-label" tbTruncateWithTooltip [innerHTML]="scheduleText"></span> |
|||
<mat-icon class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">edit</mat-icon> |
|||
</div> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,37 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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: flex; |
|||
flex: 1; |
|||
|
|||
.tb-alarm-rule-condition { |
|||
display: flex; |
|||
flex: 1; |
|||
&-button { |
|||
--mat-outlined-button-horizontal-padding: 3px 0px 12px; |
|||
display: block; |
|||
width: 100%; |
|||
} |
|||
&-label { |
|||
display: block; |
|||
text-align: start; |
|||
width: 100%; |
|||
overflow: hidden; |
|||
white-space: nowrap; |
|||
text-overflow: ellipsis; |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue