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