64 changed files with 5695 additions and 208 deletions
@ -0,0 +1,54 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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]="editDetailsFormGroup" (ngSubmit)="save()" style="width: 800px; max-width: 100%; display: grid; grid-template-rows: min-content minmax(auto, 1fr) min-content;"> |
||||
|
<mat-toolbar color="primary"> |
||||
|
<h2>{{ 'alarm-rule.edit-alarm-rule-additional-info' | translate }}</h2> |
||||
|
<span class="flex-1"></span> |
||||
|
<button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div mat-dialog-content> |
||||
|
<fieldset [disabled]="isLoading$ | async"> |
||||
|
<div class="flex flex-1 flex-col"> |
||||
|
<mat-form-field class="mat-block" appearance="outline"> |
||||
|
<textarea matInput formControlName="alarmDetails" 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> |
||||
|
</fieldset> |
||||
|
</div> |
||||
|
<div mat-dialog-actions class="flex items-center justify-end"> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" cdkFocusInitial> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button *ngIf="!data.readonly" mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty"> |
||||
|
{{ 'action.save' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,82 @@ |
|||||
|
///
|
||||
|
/// 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, OnInit, SkipSelf } from '@angular/core'; |
||||
|
import { ErrorStateMatcher } from '@angular/material/core'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { FormGroupDirective, NgForm, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { DialogComponent } from '@app/shared/components/dialog.component'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
|
||||
|
export interface AlarmRuleDetailsDialogData { |
||||
|
alarmDetails: string; |
||||
|
readonly: boolean; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-edit-alarm-details-dialog', |
||||
|
templateUrl: './alarm-rule-details-dialog.component.html', |
||||
|
providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleDetailsDialogComponent}], |
||||
|
styleUrls: [] |
||||
|
}) |
||||
|
export class AlarmRuleDetailsDialogComponent extends DialogComponent<AlarmRuleDetailsDialogComponent, string> |
||||
|
implements OnInit, ErrorStateMatcher { |
||||
|
|
||||
|
alarmDetails = this.data.alarmDetails; |
||||
|
|
||||
|
editDetailsFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleDetailsDialogData, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<AlarmRuleDetailsDialogComponent, string>, |
||||
|
private fb: UntypedFormBuilder, |
||||
|
public translate: TranslateService) { |
||||
|
super(store, router, dialogRef); |
||||
|
|
||||
|
this.editDetailsFormGroup = this.fb.group({ |
||||
|
alarmDetails: [this.alarmDetails] |
||||
|
}); |
||||
|
if (this.data.readonly) { |
||||
|
this.editDetailsFormGroup.disable(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
||||
|
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
||||
|
const customErrorState = !!(control && control.invalid && this.submitted); |
||||
|
return originalErrorState || customErrorState; |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
this.submitted = true; |
||||
|
this.alarmDetails = this.editDetailsFormGroup.get('alarmDetails').value; |
||||
|
this.dialogRef.close(this.alarmDetails); |
||||
|
} |
||||
|
} |
||||
@ -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 [formGroup]="configFormGroup"> |
||||
|
<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,70 @@ |
|||||
|
/** |
||||
|
* 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; |
||||
|
} |
||||
|
|
||||
|
.tbel-script-lang-chip { |
||||
|
line-height: 20px; |
||||
|
font-size: 14px; |
||||
|
font-weight: 500; |
||||
|
color: white; |
||||
|
border-radius: 100px; |
||||
|
width: 70px; |
||||
|
min-width: 70px; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
margin-top: 2px; |
||||
|
margin-right: 4px; |
||||
|
} |
||||
|
|
||||
|
.tb-js-func { |
||||
|
.ace_tb { |
||||
|
&.ace_calculated-field { |
||||
|
&-ctx { |
||||
|
color: #C52F00; |
||||
|
} |
||||
|
&-args { |
||||
|
color: #185F2A; |
||||
|
} |
||||
|
&-key { |
||||
|
color: #c24c1a; |
||||
|
} |
||||
|
&-time-window, &-values, &-func, &-value, &-ts, &-latestTs { |
||||
|
color: #7214D0; |
||||
|
} |
||||
|
&-start-ts, &-end-ts { |
||||
|
color: #2CAA00; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,183 @@ |
|||||
|
///
|
||||
|
/// 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, {emitEvent: true}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
addRelationType(event: MatChipInputEvent): void { |
||||
|
const input = event.chipInput.inputElement; |
||||
|
let value = event.value; |
||||
|
if ((value || '').trim()) { |
||||
|
value = value.trim(); |
||||
|
let keys: string[] = this.configFormGroup.get('propagateRelationTypes').value; |
||||
|
if (!keys || keys.indexOf(value) === -1) { |
||||
|
if (!keys) { |
||||
|
keys = []; |
||||
|
} |
||||
|
keys.push(value); |
||||
|
this.configFormGroup.get('propagateRelationTypes').setValue(keys, {emitEvent: true}); |
||||
|
} |
||||
|
} |
||||
|
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,94 @@ |
|||||
|
///
|
||||
|
/// 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, |
||||
|
CreateCfAlarmRulesComponent, |
||||
|
CfAlarmRuleComponent, |
||||
|
CfAlarmRuleConditionComponent, |
||||
|
CfAlarmRuleConditionDialogComponent, |
||||
|
CfAlarmScheduleComponent, |
||||
|
CfAlarmScheduleDialogComponent, |
||||
|
AlarmRuleFilterTextComponent, |
||||
|
AlarmRuleFilterListComponent, |
||||
|
AlarmRuleFilterDialogComponent, |
||||
|
AlarmRuleFilterPredicateListComponent, |
||||
|
AlarmRuleFilterPredicateComponent, |
||||
|
AlarmRuleFilterPredicateValueComponent, |
||||
|
AlarmRuleComplexFilterPredicateDialogComponent, |
||||
|
AlarmRuleDetailsDialogComponent, |
||||
|
] |
||||
|
}) |
||||
|
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.exportCalculatedField(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, |
||||
|
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 exportCalculatedField($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 (calculatedFieldsTableConfig) { |
||||
|
<tb-entities-table [entitiesTableConfig]="calculatedFieldsTableConfig"></tb-entities-table> |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
/** |
||||
|
* 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-drawer-container { |
||||
|
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>(); |
||||
|
|
||||
|
calculatedFieldsTableConfig: 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.calculatedFieldsTableConfig = 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,196 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div mat-dialog-content> |
||||
|
<fieldset [disabled]="isLoading$ | async"> |
||||
|
<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 [ngModel]="durationDynamicMode" |
||||
|
[ngModelOptions]="{standalone: true}" |
||||
|
(ngModelChange)="toggleDynamicMode(AlarmConditionType.DURATION)" |
||||
|
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]="durationDynamicMode"> |
||||
|
<ng-container *ngTemplateOutlet="staticValueTemplate; context:{type: AlarmConditionType.DURATION, groupName: 'value'}"></ng-container> |
||||
|
</div> |
||||
|
<div class="flex-1" [class.!hidden]="!durationDynamicMode"> |
||||
|
<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 [ngModel]="repeatingDynamicMode" |
||||
|
[ngModelOptions]="{standalone: true}" |
||||
|
(ngModelChange)="toggleDynamicMode(AlarmConditionType.REPEATING)" |
||||
|
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]="repeatingDynamicMode"> |
||||
|
<ng-container *ngTemplateOutlet="staticValueTemplate; context:{type: AlarmConditionType.REPEATING, groupName: 'count'}"></ng-container> |
||||
|
</div> |
||||
|
<div class="flex-1" [class.!hidden]="!repeatingDynamicMode"> |
||||
|
<ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{groupName: 'count'}"></ng-container> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
</section> |
||||
|
</div> |
||||
|
</fieldset> |
||||
|
</div> |
||||
|
<div mat-dialog-actions class="flex items-center justify-end"> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" cdkFocusInitial> |
||||
|
{{ (readonly ? 'action.close' : 'action.cancel') | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" |
||||
|
*ngIf="!readonly" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || 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,244 @@ |
|||||
|
///
|
||||
|
/// 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, OnInit, SkipSelf } from '@angular/core'; |
||||
|
import { ErrorStateMatcher } from '@angular/material/core'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { |
||||
|
FormGroup, |
||||
|
FormGroupDirective, |
||||
|
NgForm, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormControl, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { DialogComponent } from '@app/shared/components/dialog.component'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
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"; |
||||
|
import { FormControlsFrom } from "@shared/models/tenant.model"; |
||||
|
|
||||
|
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: [{provide: ErrorStateMatcher, useExisting: CfAlarmRuleConditionDialogComponent}], |
||||
|
styleUrls: ['./cf-alarm-rules-dialog.component.scss'], |
||||
|
}) |
||||
|
export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition> |
||||
|
implements OnInit, ErrorStateMatcher { |
||||
|
|
||||
|
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: FormGroup<FormControlsFrom<AlarmRuleCondition>>; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
readonly scriptLanguage = ScriptLanguage; |
||||
|
|
||||
|
defaultValuePlaceholder = ''; |
||||
|
defaultValueRequiredError = ''; |
||||
|
defaultValueRangeError = ''; |
||||
|
defaultValuePatternError = ''; |
||||
|
|
||||
|
durationDynamicMode = !!this.condition?.value?.dynamicValueArgument; |
||||
|
repeatingDynamicMode = !!this.condition?.count?.dynamicValueArgument; |
||||
|
|
||||
|
ComplexOperation = ComplexOperation; |
||||
|
complexOperationTranslationMap = complexOperationTranslationMap; |
||||
|
|
||||
|
functionArgs: Array<string>; |
||||
|
argumentsEditorCompleter: TbEditorCompleter; |
||||
|
argumentsHighlightRules: AceHighlightRules; |
||||
|
|
||||
|
arguments = this.data.arguments; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition>, |
||||
|
private fb: UntypedFormBuilder, |
||||
|
public translate: TranslateService) { |
||||
|
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.conditionFormGroup = this.fb.group({ |
||||
|
expression: this.fb.group({ |
||||
|
type: [this.condition?.expression?.type ?? AlarmRuleExpressionType.SIMPLE], |
||||
|
expression: [this.condition?.expression?.expression ?? null, [Validators.required]], |
||||
|
operation: [this.condition?.expression?.operation ?? ComplexOperation.AND], |
||||
|
filters: [this.condition?.expression?.filters], |
||||
|
}), |
||||
|
type: [this.condition?.type ?? AlarmRuleConditionType.SIMPLE, Validators.required], |
||||
|
unit: [this.condition?.unit ?? TimeUnit.SECONDS, Validators.required], |
||||
|
value: this.fb.group({ |
||||
|
staticValue: [this.condition?.value?.staticValue ?? null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]], |
||||
|
dynamicValueArgument: [this.condition?.value?.dynamicValueArgument ?? null, Validators.required], |
||||
|
}), |
||||
|
count: this.fb.group({ |
||||
|
staticValue: [this.condition?.count?.staticValue ?? null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]], |
||||
|
dynamicValueArgument: [this.condition?.count?.dynamicValueArgument ?? null, Validators.required], |
||||
|
}), |
||||
|
}); |
||||
|
|
||||
|
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.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE); |
||||
|
this.updateExpressionTypeValidator(this.condition?.expression?.type ?? 'SIMPLE'); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
toggleDynamicMode(type: AlarmRuleConditionType): void { |
||||
|
if (type === AlarmRuleConditionType.DURATION) { |
||||
|
this.durationDynamicMode = !this.durationDynamicMode; |
||||
|
this.updateStaticValueValidator(type, this.durationDynamicMode); |
||||
|
} else { |
||||
|
this.repeatingDynamicMode = !this.repeatingDynamicMode; |
||||
|
this.updateStaticValueValidator(type, this.repeatingDynamicMode); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateStaticValueValidator(type: AlarmRuleConditionType, dynamicValue: boolean) { |
||||
|
const control = type === AlarmRuleConditionType.DURATION ? 'value' : 'count'; |
||||
|
if (dynamicValue) { |
||||
|
this.conditionFormGroup.get(`${control}.staticValue`).disable({emitEvent: false}); |
||||
|
this.conditionFormGroup.get(`${control}.dynamicValueArgument`).enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.conditionFormGroup.get(`${control}.staticValue`).enable({emitEvent: false}); |
||||
|
this.conditionFormGroup.get(`${control}.dynamicValueArgument`).disable({emitEvent: false}); |
||||
|
} |
||||
|
this.conditionFormGroup.get(`${control}.staticValue`).updateValueAndValidity({emitEvent: false}) |
||||
|
this.conditionFormGroup.get(`${control}.dynamicValueArgument`).updateValueAndValidity({emitEvent: false}) |
||||
|
} |
||||
|
|
||||
|
updateExpressionTypeValidator(type: 'SIMPLE' | 'TBEL') { |
||||
|
if (type === 'SIMPLE') { |
||||
|
this.conditionFormGroup.get(`expression.expression`).disable(); |
||||
|
this.conditionFormGroup.get(`expression.filters`).enable(); |
||||
|
} else { |
||||
|
this.conditionFormGroup.get(`expression.expression`).enable(); |
||||
|
this.conditionFormGroup.get(`expression.filters`).disable(); |
||||
|
} |
||||
|
this.conditionFormGroup.get(`expression.expression`).updateValueAndValidity({emitEvent: false}); |
||||
|
this.conditionFormGroup.get(`expression.filters`).updateValueAndValidity({emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
||||
|
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
||||
|
const customErrorState = !!(control && control.invalid && this.submitted); |
||||
|
return originalErrorState || customErrorState; |
||||
|
} |
||||
|
|
||||
|
private updateValidators(type: AlarmRuleConditionType, emitEvent = false) { |
||||
|
switch (type) { |
||||
|
case AlarmRuleConditionType.DURATION: |
||||
|
this.conditionFormGroup.get('unit').enable(); |
||||
|
this.conditionFormGroup.get('value').enable(); |
||||
|
this.conditionFormGroup.get('count').disable(); |
||||
|
|
||||
|
this.updateStaticValueValidator(type, this.durationDynamicMode); |
||||
|
|
||||
|
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(); |
||||
|
this.conditionFormGroup.get('value').disable(); |
||||
|
this.conditionFormGroup.get('unit').disable(); |
||||
|
|
||||
|
this.updateStaticValueValidator(type, this.repeatingDynamicMode); |
||||
|
|
||||
|
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(); |
||||
|
this.conditionFormGroup.get('count').disable(); |
||||
|
this.conditionFormGroup.get('unit').disable(); |
||||
|
break; |
||||
|
} |
||||
|
this.conditionFormGroup.get('value').updateValueAndValidity({emitEvent}); |
||||
|
this.conditionFormGroup.get('count').updateValueAndValidity({emitEvent}); |
||||
|
this.conditionFormGroup.get('unit').updateValueAndValidity({emitEvent}); |
||||
|
} |
||||
|
|
||||
|
get argumentsList(): Array<string> { |
||||
|
return this.arguments ? Object.keys(this.arguments): []; |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
this.submitted = true; |
||||
|
this.condition = this.conditionFormGroup.value as AlarmRuleCondition; |
||||
|
this.dialogRef.close(this.condition); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,283 @@ |
|||||
|
///
|
||||
|
/// 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 { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormControl, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { deepClone, isDefinedAndNotNull } from '@core/utils'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { |
||||
|
dayOfWeekTranslations, |
||||
|
getAlarmScheduleRangeText, |
||||
|
utcTimestampToTimeOfDay |
||||
|
} from '@shared/models/device.models'; |
||||
|
import { TimeUnit } from '@shared/models/time/time.models'; |
||||
|
import { |
||||
|
CfAlarmRuleConditionDialogComponent, |
||||
|
CfAlarmRuleConditionDialogData |
||||
|
} from "@home/components/alarm-rules/cf-alarm-rule-condition-dialog.component"; |
||||
|
import { |
||||
|
AlarmRuleCondition, |
||||
|
AlarmRuleConditionType, |
||||
|
AlarmRuleSchedule, |
||||
|
AlarmRuleScheduleType |
||||
|
} from "@shared/models/alarm-rule.models"; |
||||
|
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; |
||||
|
import { |
||||
|
AlarmRuleScheduleDialogData, |
||||
|
CfAlarmScheduleDialogComponent |
||||
|
} from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-cf-alarm-rule-condition', |
||||
|
templateUrl: './cf-alarm-rule-condition.component.html', |
||||
|
styleUrls: ['./cf-alarm-rule-condition.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => CfAlarmRuleConditionComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => CfAlarmRuleConditionComponent), |
||||
|
multi: true, |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class CfAlarmRuleConditionComponent implements ControlValueAccessor, OnInit, Validator { |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
arguments: Record<string, CalculatedFieldArgument>; |
||||
|
|
||||
|
alarmRuleConditionFormGroup = this.fb.group({ |
||||
|
type: ['SIMPLE'], |
||||
|
expression: [null, Validators.required], |
||||
|
schedule: [null], |
||||
|
}); |
||||
|
|
||||
|
specText = ''; |
||||
|
|
||||
|
scheduleText = ''; |
||||
|
|
||||
|
private modelValue: AlarmRuleCondition; |
||||
|
|
||||
|
private propagateChange = (v: any) => { }; |
||||
|
|
||||
|
constructor(private dialog: MatDialog, |
||||
|
private fb: UntypedFormBuilder, |
||||
|
private cd: ChangeDetectorRef, |
||||
|
private translate: TranslateService) { |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.alarmRuleConditionFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.alarmRuleConditionFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: AlarmRuleCondition): void { |
||||
|
this.modelValue = value; |
||||
|
this.updateConditionInfo(); |
||||
|
} |
||||
|
|
||||
|
public conditionSet() { |
||||
|
return this.modelValue && (this.modelValue.expression.expression || this.modelValue.expression.filters); |
||||
|
} |
||||
|
|
||||
|
public validate(c: UntypedFormControl) { |
||||
|
return this.conditionSet() ? null : { |
||||
|
alarmRuleCondition: { |
||||
|
valid: false, |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public openFilterDialog($event: Event) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.dialog.open<CfAlarmRuleConditionDialogComponent, CfAlarmRuleConditionDialogData, |
||||
|
AlarmRuleCondition>(CfAlarmRuleConditionDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
readonly: this.disabled, |
||||
|
condition: this.disabled ? this.modelValue : deepClone(this.modelValue), |
||||
|
arguments: this.arguments |
||||
|
} |
||||
|
}).afterClosed().subscribe((result) => { |
||||
|
if (result) { |
||||
|
this.modelValue = {...this.modelValue, ...result}; |
||||
|
this.updateModel(); |
||||
|
this.updateSpecText(); |
||||
|
this.cd.detectChanges(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateConditionInfo() { |
||||
|
this.alarmRuleConditionFormGroup.patchValue( |
||||
|
{ |
||||
|
type: this.modelValue?.type, |
||||
|
expression: this.modelValue?.expression, |
||||
|
schedule: this.modelValue?.schedule, |
||||
|
}, {emitEvent: false} |
||||
|
); |
||||
|
this.updateScheduleText(); |
||||
|
this.updateSpecText(); |
||||
|
} |
||||
|
|
||||
|
private updateSpecText() { |
||||
|
this.specText = ''; |
||||
|
if (this.modelValue && this.modelValue.type) { |
||||
|
const type = this.modelValue.type; |
||||
|
switch (type) { |
||||
|
case AlarmRuleConditionType.SIMPLE: |
||||
|
break; |
||||
|
case AlarmRuleConditionType.DURATION: |
||||
|
let duringText = ''; |
||||
|
switch (this.modelValue.unit) { |
||||
|
case TimeUnit.SECONDS: |
||||
|
duringText = this.translate.instant('timewindow.seconds', {seconds: this.modelValue.value.staticValue}); |
||||
|
break; |
||||
|
case TimeUnit.MINUTES: |
||||
|
duringText = this.translate.instant('timewindow.minutes', {minutes: this.modelValue.value.staticValue}); |
||||
|
break; |
||||
|
case TimeUnit.HOURS: |
||||
|
duringText = this.translate.instant('timewindow.hours', {hours: this.modelValue.value.staticValue}); |
||||
|
break; |
||||
|
case TimeUnit.DAYS: |
||||
|
duringText = this.translate.instant('timewindow.days', {days: this.modelValue.value.staticValue}); |
||||
|
break; |
||||
|
} |
||||
|
if (this.modelValue.value.dynamicValueArgument) { |
||||
|
this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', { |
||||
|
attribute: `${this.modelValue.value.dynamicValueArgument}` |
||||
|
}); |
||||
|
} else { |
||||
|
this.specText = this.translate.instant('alarm-rule.condition-during', { |
||||
|
during: duringText |
||||
|
}); |
||||
|
} |
||||
|
break; |
||||
|
case AlarmRuleConditionType.REPEATING: |
||||
|
if (this.modelValue.count.dynamicValueArgument) { |
||||
|
this.specText = this.translate.instant('alarm-rule.condition-repeat-times-dynamic', { |
||||
|
attribute: `${this.modelValue.count.dynamicValueArgument}` |
||||
|
}); |
||||
|
} else { |
||||
|
this.specText = this.translate.instant('alarm-rule.condition-repeat-times', |
||||
|
{count: this.modelValue.count.staticValue}); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
this.updateConditionInfo(); |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
|
||||
|
public openScheduleDialog($event: Event) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.dialog.open<CfAlarmScheduleDialogComponent, AlarmRuleScheduleDialogData, |
||||
|
AlarmRuleSchedule>(CfAlarmScheduleDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
readonly: this.disabled, |
||||
|
alarmSchedule: this.disabled ? this.modelValue?.schedule : deepClone(this.modelValue?.schedule), |
||||
|
arguments: this.arguments |
||||
|
} |
||||
|
}).afterClosed().subscribe((result) => { |
||||
|
if (result) { |
||||
|
this.modelValue.schedule = result; |
||||
|
this.propagateChange(this.modelValue); |
||||
|
this.updateScheduleText(); |
||||
|
this.cd.detectChanges(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateScheduleText() { |
||||
|
let schedule = this.modelValue?.schedule; |
||||
|
this.scheduleText = ''; |
||||
|
if (isDefinedAndNotNull(schedule)) { |
||||
|
if (schedule.dynamicValueArgument) { |
||||
|
this.scheduleText = this.translate.instant('alarm-rule.value-argument') + ': ' + schedule?.dynamicValueArgument |
||||
|
} else { |
||||
|
switch (schedule.staticValue.type) { |
||||
|
case AlarmRuleScheduleType.ANY_TIME: |
||||
|
this.scheduleText = this.translate.instant('alarm-rule.schedule.any-time'); |
||||
|
break; |
||||
|
case AlarmRuleScheduleType.SPECIFIC_TIME: |
||||
|
for (const day of schedule.staticValue.daysOfWeek) { |
||||
|
if (this.scheduleText.length) { |
||||
|
this.scheduleText += ', '; |
||||
|
} |
||||
|
this.scheduleText += this.translate.instant(dayOfWeekTranslations[day - 1]); |
||||
|
} |
||||
|
this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(schedule.staticValue.startsOn), |
||||
|
utcTimestampToTimeOfDay(schedule.staticValue.endsOn)) + '</b>'; |
||||
|
break; |
||||
|
case AlarmRuleScheduleType.CUSTOM: |
||||
|
for (const item of schedule.staticValue.items) { |
||||
|
if (item.enabled) { |
||||
|
if (this.scheduleText.length) { |
||||
|
this.scheduleText += ', '; |
||||
|
} |
||||
|
this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]); |
||||
|
this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn), |
||||
|
utcTimestampToTimeOfDay(item.endsOn)) + '</b>'; |
||||
|
} |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if (!this.scheduleText.length) { |
||||
|
this.scheduleText = this.translate.instant('alarm-rule.schedule.any-time'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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-form-panel no-border no-padding" [formGroup]="alarmRuleFormGroup"> |
||||
|
<tb-cf-alarm-rule-condition formControlName="condition" [arguments]="arguments"> |
||||
|
</tb-cf-alarm-rule-condition> |
||||
|
@if (!disabled || alarmRuleFormGroup.get('dashboardId').value) { |
||||
|
<div class="tb-form-row space-between column-xs"> |
||||
|
<div class="min-w-40 xs:min-w-fit" translate> |
||||
|
alarm-rule.alarm-rule-additional-info |
||||
|
</div> |
||||
|
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="alarmDetails" placeholder="{{ 'alarm-rule.alarm-rule-additional-info' | translate }}"> |
||||
|
<button type="button" |
||||
|
matSuffix mat-icon-button aria-label="Open in new" |
||||
|
(click)="openEditDetailsDialog($event)"> |
||||
|
<mat-icon class="material-icons">open_in_new</mat-icon> |
||||
|
</button> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
} |
||||
|
@if (!disabled || alarmRuleFormGroup.get('dashboardId').value) { |
||||
|
<div class="tb-form-row space-between column-xs"> |
||||
|
<div class="min-w-40 xs:min-w-fit" tb-hint-tooltip-icon="{{'alarm-rule.alarm-rule-mobile-dashboard-hint' | translate}}" translate> |
||||
|
alarm-rule.alarm-rule-mobile-dashboard |
||||
|
</div> |
||||
|
<tb-dashboard-autocomplete appearance="outline" |
||||
|
subscriptSizing="dynamic" |
||||
|
inlineField |
||||
|
class="flex-1" |
||||
|
placeholder="{{ 'alarm-rule.alarm-rule-mobile-dashboard' | translate }}" |
||||
|
formControlName="dashboardId"> |
||||
|
</tb-dashboard-autocomplete> |
||||
|
</div> |
||||
|
} |
||||
|
</div> |
||||
@ -0,0 +1,35 @@ |
|||||
|
/** |
||||
|
* 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 { |
||||
|
min-width: 0; |
||||
|
.row { |
||||
|
margin-top: 1em; |
||||
|
} |
||||
|
.tb-alarm-rule-details, .tb-alarm-rule-dashboard { |
||||
|
padding: 4px; |
||||
|
&.title { |
||||
|
opacity: 0.7; |
||||
|
overflow: visible; |
||||
|
} |
||||
|
} |
||||
|
.tb-alarm-rule-details { |
||||
|
overflow: hidden; |
||||
|
white-space: nowrap; |
||||
|
text-overflow: ellipsis; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,153 @@ |
|||||
|
///
|
||||
|
/// 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, forwardRef, Input, OnInit } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormControl, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
||||
|
import { isDefinedAndNotNull } from '@core/utils'; |
||||
|
import { DashboardId } from '@shared/models/id/dashboard-id'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
import { AlarmRule } from "@shared/models/alarm-rule.models"; |
||||
|
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; |
||||
|
import { |
||||
|
AlarmRuleDetailsDialogComponent, |
||||
|
AlarmRuleDetailsDialogData |
||||
|
} from "@home/components/alarm-rules/alarm-rule-details-dialog.component"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-cf-alarm-rule', |
||||
|
templateUrl: './cf-alarm-rule.component.html', |
||||
|
styleUrls: ['./cf-alarm-rule.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => CfAlarmRuleComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => CfAlarmRuleComponent), |
||||
|
multi: true, |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
private requiredValue: boolean; |
||||
|
get required(): boolean { |
||||
|
return this.requiredValue; |
||||
|
} |
||||
|
@Input() |
||||
|
set required(value: boolean) { |
||||
|
this.requiredValue = coerceBooleanProperty(value); |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
arguments: Record<string, CalculatedFieldArgument>; |
||||
|
|
||||
|
private modelValue: AlarmRule; |
||||
|
|
||||
|
alarmRuleFormGroup= this.fb.group({ |
||||
|
condition: [null, [Validators.required]], |
||||
|
alarmDetails: [null], |
||||
|
dashboardId: [null] |
||||
|
}); |
||||
|
|
||||
|
private propagateChange = (v: any) => { }; |
||||
|
|
||||
|
constructor(private dialog: MatDialog, |
||||
|
private fb: UntypedFormBuilder, |
||||
|
private destroyRef: DestroyRef) { |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.alarmRuleFormGroup.valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.alarmRuleFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.alarmRuleFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: AlarmRule): void { |
||||
|
this.modelValue = value; |
||||
|
const model = this.modelValue ? { |
||||
|
...this.modelValue, |
||||
|
dashboardId: this.modelValue.dashboardId?.id |
||||
|
} : null; |
||||
|
this.alarmRuleFormGroup.patchValue(model, {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
public openEditDetailsDialog($event: Event) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.dialog.open<AlarmRuleDetailsDialogComponent, AlarmRuleDetailsDialogData, |
||||
|
string>(AlarmRuleDetailsDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value, |
||||
|
readonly: this.disabled |
||||
|
} |
||||
|
}).afterClosed().subscribe((alarmDetails) => { |
||||
|
if (isDefinedAndNotNull(alarmDetails)) { |
||||
|
this.alarmRuleFormGroup.patchValue({alarmDetails}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public validate(c: UntypedFormControl) { |
||||
|
return (!this.required && !this.modelValue || this.alarmRuleFormGroup.valid) ? null : { |
||||
|
alarmRule: { |
||||
|
valid: false, |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
const value = this.alarmRuleFormGroup.value; |
||||
|
this.modelValue = {...this.modelValue, ...value, dashboardId: value.dashboardId ? new DashboardId(value.dashboardId) : null}; |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
/** |
||||
|
* 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 { |
||||
|
form { |
||||
|
width: 900px; |
||||
|
max-width: 100%; |
||||
|
display: grid; |
||||
|
grid-template-rows: min-content minmax(auto, 1fr) min-content; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,54 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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]="alarmScheduleFormGroup" (ngSubmit)="save()"> |
||||
|
<mat-toolbar color="primary"> |
||||
|
<h2>{{ (readonly ? 'alarm-rule.schedule-title' : 'alarm-rule.edit-schedule') | translate }}</h2> |
||||
|
<span class="flex-1"></span> |
||||
|
<tb-toggle-select [(ngModel)]="settingsMode" [ngModelOptions]="{standalone: true}" appearance="fill-invert" selectMediaBreakpoint="xs"> |
||||
|
<tb-toggle-option value="static">{{ 'alarm-rule.static-schedule' | translate }}</tb-toggle-option> |
||||
|
<tb-toggle-option value="dynamic">{{ 'alarm-rule.dynamic-schedule' | translate }}</tb-toggle-option> |
||||
|
</tb-toggle-select> |
||||
|
<button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div mat-dialog-content> |
||||
|
<tb-cf-alarm-schedule [arguments]="arguments" |
||||
|
[settingsMode]="settingsMode" |
||||
|
formControlName="alarmSchedule"> |
||||
|
</tb-cf-alarm-schedule> |
||||
|
</div> |
||||
|
<div mat-dialog-actions class="flex items-center justify-end"> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" cdkFocusInitial> |
||||
|
{{ (readonly ? 'action.close' : 'action.cancel') | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" |
||||
|
*ngIf="!readonly" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || alarmScheduleFormGroup.invalid || !alarmScheduleFormGroup.dirty"> |
||||
|
{{ 'action.save' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,92 @@ |
|||||
|
///
|
||||
|
/// 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, OnInit, SkipSelf } from '@angular/core'; |
||||
|
import { ErrorStateMatcher } from '@angular/material/core'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { FormGroupDirective, NgForm, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { DialogComponent } from '@app/shared/components/dialog.component'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { AlarmRuleSchedule } from "@shared/models/alarm-rule.models"; |
||||
|
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; |
||||
|
|
||||
|
export interface AlarmRuleScheduleDialogData { |
||||
|
readonly: boolean; |
||||
|
alarmSchedule: AlarmRuleSchedule; |
||||
|
arguments: Record<string, CalculatedFieldArgument>; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-cf-alarm-schedule-dialog', |
||||
|
templateUrl: './cf-alarm-schedule-dialog.component.html', |
||||
|
providers: [{provide: ErrorStateMatcher, useExisting: CfAlarmScheduleDialogComponent}], |
||||
|
styleUrls: ['./cf-alarm-rules-dialog.component.scss'], |
||||
|
}) |
||||
|
export class CfAlarmScheduleDialogComponent extends DialogComponent<CfAlarmScheduleDialogComponent, AlarmRuleSchedule> |
||||
|
implements OnInit, ErrorStateMatcher { |
||||
|
|
||||
|
readonly = this.data.readonly; |
||||
|
alarmSchedule = this.data.alarmSchedule; |
||||
|
arguments = this.data.arguments; |
||||
|
|
||||
|
alarmScheduleFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
settingsMode: 'static' | 'dynamic' = 'static'; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleScheduleDialogData, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<CfAlarmScheduleDialogComponent, AlarmRuleSchedule>, |
||||
|
private fb: UntypedFormBuilder, |
||||
|
private utils: UtilsService, |
||||
|
public translate: TranslateService) { |
||||
|
super(store, router, dialogRef); |
||||
|
|
||||
|
this.alarmScheduleFormGroup = this.fb.group({ |
||||
|
alarmSchedule: [this.alarmSchedule] |
||||
|
}); |
||||
|
this.settingsMode = this.alarmSchedule?.dynamicValueArgument ? 'dynamic' : 'static'; |
||||
|
if (this.readonly) { |
||||
|
this.alarmScheduleFormGroup.disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
||||
|
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
||||
|
const customErrorState = !!(control && control.invalid && this.submitted); |
||||
|
return originalErrorState || customErrorState; |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
this.submitted = true; |
||||
|
this.alarmSchedule = this.alarmScheduleFormGroup.get('alarmSchedule').value; |
||||
|
this.dialogRef.close(this.alarmSchedule); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,124 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<section [formGroup]="alarmScheduleForm" class="flex flex-col"> |
||||
|
@if (settingsMode === 'static') { |
||||
|
<ng-container formGroupName="staticValue"> |
||||
|
<mat-form-field class="mat-block" appearance="outline"> |
||||
|
<mat-select formControlName="type" required placeholder="{{ 'alarm-rule.schedule-type' | translate }}"> |
||||
|
<mat-option *ngFor="let alarmScheduleType of alarmScheduleTypes" [value]="alarmScheduleType"> |
||||
|
{{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
<mat-error *ngIf="alarmScheduleForm.get('staticValue.type').hasError('required')"> |
||||
|
{{ 'alarm-rule.schedule-type-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</ng-container> |
||||
|
@if (alarmScheduleForm.get('staticValue.type').value !== alarmScheduleType.ANY_TIME) { |
||||
|
<div class="tb-form-panel no-padding no-border"> |
||||
|
<div formGroupName="staticValue"> |
||||
|
<tb-timezone-select |
||||
|
appearance="outline" |
||||
|
userTimezoneByDefault |
||||
|
required |
||||
|
formControlName="timezone"> |
||||
|
</tb-timezone-select> |
||||
|
<section *ngIf="alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.SPECIFIC_TIME"> |
||||
|
<mat-chip-listbox multiple formControlName="daysOfWeek"> |
||||
|
<mat-chip-option *ngFor="let day of allDays; let i = index;" |
||||
|
[value]="i+1"> |
||||
|
{{ dayOfWeekTranslationsArray[day] | translate }} |
||||
|
</mat-chip-option> |
||||
|
</mat-chip-listbox> |
||||
|
<tb-error class="block mb-2" [error]="alarmScheduleForm.get('staticValue.daysOfWeek').hasError('required') |
||||
|
? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error> |
||||
|
<div class="flex flex-row xs:flex-col gt-xs:gap-2"> |
||||
|
<div class="flex flex-row gap-2 gt-md:flex-1"> |
||||
|
<mat-form-field class="flex-1 sm:min-w-37.5 sm:max-w-37.5 sm:basis-37.5 md:min-w-37.5 md:max-w-37.5 md:basis-37.5" appearance="outline"> |
||||
|
<mat-label translate>alarm-rule.schedule-time-from</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="flex-1 sm:min-w-37.5 sm:max-w-37.5 sm:basis-37.5 md:min-w-37.5 md:max-w-37.5 md:basis-37.5" appearance="outline"> |
||||
|
<mat-label translate>alarm-rule.schedule-time-to</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="flex flex-1 items-center justify-center mb-[22px]"> |
||||
|
<div style="text-align: center" |
||||
|
[innerHTML]="getSchedulerRangeText(alarmScheduleForm.get('staticValue'))"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
<section *ngIf="alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.CUSTOM" class="flex flex-col gap-4"> |
||||
|
<div *ngFor="let day of allDays" class="flex flex-col gap-4" formArrayName="items"> |
||||
|
<div class="flex flex-row items-center justify-start gap-2 xs:flex-col xs:items-start xs:justify-center" [formGroupName]="''+day"> |
||||
|
<mat-chip-listbox formControlName="enabled"> |
||||
|
<mat-chip-option class="w-32" color="primary" (selectionChange)="changeCustomScheduler($event, day)" [value]="true">{{ dayOfWeekTranslationsArray[day] | translate }}</mat-chip-option> |
||||
|
</mat-chip-listbox> |
||||
|
<div class="flex flex-1 flex-row gap-2"> |
||||
|
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-label translate>alarm-rule.schedule-time-from</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-label translate>alarm-rule.schedule-time-to</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="flex flex-1 items-center justify-center sm:max-w-[120px] mb-[22px]" |
||||
|
style="text-align: center" |
||||
|
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<tb-error style="display: block;" [error]="alarmScheduleForm.get('staticValue.items').hasError('dayOfWeeks') |
||||
|
? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error> |
||||
|
</section> |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
} @else { |
||||
|
<div class="flex flex-row items-center gap-2"> |
||||
|
<mat-form-field class="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="alarmScheduleForm.get('dynamicValueArgument').hasError('required')"> |
||||
|
{{ 'calculated-fields.hint.argument-name-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<div matSuffix style="height: 56px" |
||||
|
[tb-help-popup]="'alarm-rule/alarm_rule_schedule_format'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
</section> |
||||
@ -0,0 +1,325 @@ |
|||||
|
///
|
||||
|
/// 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, forwardRef, Input, OnInit } from '@angular/core'; |
||||
|
import { |
||||
|
AbstractControl, |
||||
|
ControlValueAccessor, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormArray, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormGroup, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { |
||||
|
dayOfWeekTranslations, |
||||
|
getAlarmScheduleRangeText, |
||||
|
timeOfDayToUTCTimestamp, |
||||
|
utcTimestampToTimeOfDay |
||||
|
} from '@shared/models/device.models'; |
||||
|
import { isDefined } from '@core/utils'; |
||||
|
import { getDefaultTimezone } from '@shared/models/time/time.models'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
import { |
||||
|
AlarmRuleSchedule, |
||||
|
AlarmRuleScheduleType, |
||||
|
AlarmRuleScheduleTypeTranslationMap |
||||
|
} from "@shared/models/alarm-rule.models"; |
||||
|
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; |
||||
|
import { MatChipSelectionChange } from "@angular/material/chips"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-cf-alarm-schedule', |
||||
|
templateUrl: './cf-alarm-schedule.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => CfAlarmScheduleComponent), |
||||
|
multi: true |
||||
|
}, { |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => CfAlarmScheduleComponent), |
||||
|
multi: true |
||||
|
}] |
||||
|
}) |
||||
|
export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit { |
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
arguments: Record<string, CalculatedFieldArgument>; |
||||
|
|
||||
|
private settingsModeValue: 'static' | 'dynamic'; |
||||
|
get settingsMode(): 'static' | 'dynamic' { |
||||
|
return this.settingsModeValue; |
||||
|
} |
||||
|
@Input() |
||||
|
set settingsMode(value: 'static' | 'dynamic') { |
||||
|
if (value !== this.settingsModeValue && this.alarmScheduleForm) { |
||||
|
this.settingsModeValue = value; |
||||
|
this.updateModeValidators(value); |
||||
|
this.updateModel(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
alarmScheduleForm = this.fb.group({ |
||||
|
staticValue: this.fb.group({ |
||||
|
type: [AlarmRuleScheduleType.ANY_TIME, Validators.required], |
||||
|
timezone: [null, Validators.required], |
||||
|
daysOfWeek: [null, Validators.required], |
||||
|
startsOn: [0, Validators.required], |
||||
|
endsOn: [0, Validators.required], |
||||
|
items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems), |
||||
|
}), |
||||
|
dynamicValueArgument: [null, Validators.required] |
||||
|
}); |
||||
|
|
||||
|
alarmScheduleTypes = Object.keys(AlarmRuleScheduleType); |
||||
|
alarmScheduleType = AlarmRuleScheduleType; |
||||
|
alarmScheduleTypeTranslate = AlarmRuleScheduleTypeTranslationMap; |
||||
|
dayOfWeekTranslationsArray = dayOfWeekTranslations; |
||||
|
|
||||
|
allDays = Array(7).fill(0).map((x, i) => i); |
||||
|
|
||||
|
private modelValue: AlarmRuleSchedule; |
||||
|
|
||||
|
private defaultItems = Array.from({length: 7}, (value, i) => ({ |
||||
|
enabled: true, |
||||
|
dayOfWeek: i + 1 |
||||
|
})); |
||||
|
|
||||
|
private propagateChange = (v: any) => { }; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private destroyRef: DestroyRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.alarmScheduleForm.get('staticValue.type').valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe((type) => { |
||||
|
const defaultTimezone = getDefaultTimezone(); |
||||
|
this.alarmScheduleForm.get('staticValue').patchValue({type, items: this.defaultItems, timezone: defaultTimezone}, {emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('dynamicValueArgument').patchValue(null, {emitEvent: false}); |
||||
|
this.updateValidators(type); |
||||
|
this.alarmScheduleForm.updateValueAndValidity(); |
||||
|
}); |
||||
|
this.alarmScheduleForm.valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
|
||||
|
this.alarmScheduleForm.get('staticValue.items').valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe((items) => { |
||||
|
items.forEach((item, index) => this.disabledSelectedTime(item.enabled, index, false)) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
validateItems(control: AbstractControl): ValidationErrors | null { |
||||
|
const items: any[] = control.value; |
||||
|
if (!items || !items.length || !items.find(v => v.enabled === true)) { |
||||
|
return { |
||||
|
dayOfWeeks: true |
||||
|
}; |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.alarmScheduleForm.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.updateModeValidators(this.settingsMode); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: AlarmRuleSchedule): void { |
||||
|
if (value) { |
||||
|
this.modelValue = value; |
||||
|
if (this.modelValue.dynamicValueArgument) { |
||||
|
this.settingsModeValue = 'dynamic'; |
||||
|
this.alarmScheduleForm.get('dynamicValueArgument').patchValue(this.modelValue.dynamicValueArgument, {emitEvent: false}); |
||||
|
} else { |
||||
|
this.settingsModeValue = 'static'; |
||||
|
switch (this.modelValue.staticValue.type) { |
||||
|
case AlarmRuleScheduleType.SPECIFIC_TIME: |
||||
|
this.alarmScheduleForm.patchValue({ |
||||
|
staticValue: { |
||||
|
type: this.modelValue.staticValue.type, |
||||
|
timezone: this.modelValue.staticValue.timezone, |
||||
|
daysOfWeek: this.modelValue.staticValue.daysOfWeek, |
||||
|
startsOn: utcTimestampToTimeOfDay(this.modelValue.staticValue.startsOn), |
||||
|
endsOn: utcTimestampToTimeOfDay(this.modelValue.staticValue.endsOn), |
||||
|
}, |
||||
|
}, {emitEvent: false}); |
||||
|
break; |
||||
|
case AlarmRuleScheduleType.CUSTOM: |
||||
|
if (this.modelValue?.dynamicValueArgument) { |
||||
|
this.alarmScheduleForm.patchValue({ |
||||
|
staticValue: { |
||||
|
type: this.modelValue.staticValue.type, |
||||
|
}, |
||||
|
}, {emitEvent: false}); |
||||
|
} else if (this.modelValue.staticValue?.items) { |
||||
|
const alarmDays = []; |
||||
|
this.modelValue.staticValue.items |
||||
|
.sort((a, b) => a.dayOfWeek - b.dayOfWeek) |
||||
|
.forEach((item, index) => { |
||||
|
this.disabledSelectedTime(item.enabled, index); |
||||
|
alarmDays.push({ |
||||
|
enabled: item.enabled, |
||||
|
startsOn: utcTimestampToTimeOfDay(item.startsOn), |
||||
|
endsOn: utcTimestampToTimeOfDay(item.endsOn) |
||||
|
}); |
||||
|
}); |
||||
|
this.alarmScheduleForm.patchValue({ |
||||
|
staticValue: { |
||||
|
type: this.modelValue.staticValue.type, |
||||
|
timezone: this.modelValue.staticValue.timezone, |
||||
|
items: alarmDays, |
||||
|
}, |
||||
|
}, {emitEvent: false}); |
||||
|
} |
||||
|
break; |
||||
|
default: |
||||
|
this.alarmScheduleForm.patchValue(this.modelValue || undefined, {emitEvent: false}); |
||||
|
} |
||||
|
this.updateValidators(this.modelValue.staticValue.type); |
||||
|
} |
||||
|
this.updateModeValidators(this.settingsMode); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
validate(control: UntypedFormGroup): ValidationErrors | null { |
||||
|
return this.alarmScheduleForm.valid ? null : { |
||||
|
alarmScheduler: { |
||||
|
valid: false |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private updateModeValidators(mode: 'static' | 'dynamic') { |
||||
|
if (mode === 'static') { |
||||
|
this.alarmScheduleForm.get('staticValue').enable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('dynamicValueArgument').disable({emitEvent: false}); |
||||
|
this.updateValidators(this.alarmScheduleForm.get('staticValue.type').value); |
||||
|
} else { |
||||
|
this.alarmScheduleForm.get('staticValue').disable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('dynamicValueArgument').enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateValidators(type: AlarmRuleScheduleType){ |
||||
|
switch (type){ |
||||
|
case AlarmRuleScheduleType.ANY_TIME: |
||||
|
this.alarmScheduleForm.get('staticValue.timezone').disable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('staticValue.daysOfWeek').disable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('staticValue.startsOn').disable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('staticValue.endsOn').disable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('staticValue.items').disable({emitEvent: false}); |
||||
|
break; |
||||
|
case AlarmRuleScheduleType.SPECIFIC_TIME: |
||||
|
this.alarmScheduleForm.get('staticValue.timezone').enable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('staticValue.daysOfWeek').enable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('staticValue.startsOn').enable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('staticValue.endsOn').enable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('staticValue.items').disable({emitEvent: false}); |
||||
|
break; |
||||
|
case AlarmRuleScheduleType.CUSTOM: |
||||
|
this.alarmScheduleForm.get('staticValue.timezone').enable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('staticValue.daysOfWeek').disable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('staticValue.startsOn').disable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('staticValue.endsOn').disable({emitEvent: false}); |
||||
|
this.alarmScheduleForm.get('staticValue.items').enable({emitEvent: true}); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
const value = this.alarmScheduleForm.value as AlarmRuleSchedule; |
||||
|
if (this.settingsMode === 'static') { |
||||
|
if (isDefined(value.staticValue.startsOn) && value.staticValue.startsOn !== 0) { |
||||
|
value.staticValue.startsOn = timeOfDayToUTCTimestamp(value.staticValue.startsOn); |
||||
|
} |
||||
|
if (isDefined(value.staticValue.endsOn) && value.staticValue.endsOn !== 0) { |
||||
|
value.staticValue.endsOn = timeOfDayToUTCTimestamp(value.staticValue.endsOn); |
||||
|
} |
||||
|
if (isDefined(value.staticValue.items)){ |
||||
|
value.staticValue.items = this.alarmScheduleForm.getRawValue().staticValue.items; |
||||
|
value.staticValue.items = value.staticValue.items.map((item) => { |
||||
|
return { ...item, startsOn: timeOfDayToUTCTimestamp(item.startsOn), endsOn: timeOfDayToUTCTimestamp(item.endsOn)}; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
this.modelValue = value; |
||||
|
if (this.alarmScheduleForm.valid) { |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} else { |
||||
|
this.propagateChange(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private defaultItemsScheduler(index): UntypedFormGroup { |
||||
|
return this.fb.group({ |
||||
|
enabled: [true], |
||||
|
dayOfWeek: [index + 1], |
||||
|
startsOn: [0, Validators.required], |
||||
|
endsOn: [0, Validators.required] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
changeCustomScheduler($event: MatChipSelectionChange, index: number) { |
||||
|
const value = $event.selected; |
||||
|
this.disabledSelectedTime(value, index, true); |
||||
|
} |
||||
|
|
||||
|
private disabledSelectedTime(enable: boolean, index: number, emitEvent = false) { |
||||
|
if (enable) { |
||||
|
this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false}); |
||||
|
this.itemsSchedulerForm.at(index).get('endsOn').enable({emitEvent}); |
||||
|
} else { |
||||
|
this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false}); |
||||
|
this.itemsSchedulerForm.at(index).get('endsOn').disable({emitEvent}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
getSchedulerRangeText(control: UntypedFormGroup | AbstractControl): string { |
||||
|
return getAlarmScheduleRangeText(control.get('startsOn').value, control.get('endsOn').value); |
||||
|
} |
||||
|
|
||||
|
get itemsSchedulerForm(): UntypedFormArray { |
||||
|
return this.alarmScheduleForm.get('staticValue.items') as UntypedFormArray; |
||||
|
} |
||||
|
|
||||
|
get argumentsList(): Array<string> { |
||||
|
return this.arguments ? Object.keys(this.arguments): []; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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. |
||||
|
|
||||
|
--> |
||||
|
<!-- |
||||
|
|
||||
|
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="flex flex-1 flex-col"> |
||||
|
@for (createAlarmRuleControl of createAlarmRulesFormArray().controls; track createAlarmRuleControl; let index = $index) { |
||||
|
<div class="flex flex-row items-center justify-start gap-2 pb-2" [formGroup]="createAlarmRuleControl"> |
||||
|
<div class="create-alarm-rule flex flex-1 flex-col gap-4" [style.border-left-color]="AlarmSeverityNotificationColors.get(createAlarmRuleControl.get('severity').value)"> |
||||
|
<div class="tb-form-row space-between column-xs"> |
||||
|
<div class="min-w-40 xs:min-w-fit" translate>alarm.severity</div> |
||||
|
<mat-form-field appearance="outline" class="flex-1 xs:w-full" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="severity" |
||||
|
required |
||||
|
placeholder="{{ 'alarm-rule.select-alarm-severity' | translate }}"> |
||||
|
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity" |
||||
|
[disabled]="isDisabledSeverity(alarmSeverityEnum[alarmSeverity], index)"> |
||||
|
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<tb-cf-alarm-rule formControlName="alarmRule" [arguments]="arguments" required class="flex-1"> |
||||
|
</tb-cf-alarm-rule> |
||||
|
</div> |
||||
|
<button *ngIf="!disabled" |
||||
|
class="button-icon" |
||||
|
mat-icon-button |
||||
|
type="button" |
||||
|
(click)="removeCreateAlarmRule(index)" |
||||
|
matTooltip="{{ 'action.remove' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>delete</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
} |
||||
|
<div *ngIf="!createAlarmRulesFormArray().controls.length && !disabled"> |
||||
|
<span translate class="flex items-center justify-center tb-prompt required" style="margin: 16px 0"> |
||||
|
alarm-rule.add-create-alarm-rule-prompt |
||||
|
</span> |
||||
|
</div> |
||||
|
<div *ngIf="!disabled"> |
||||
|
<button mat-stroked-button color="primary" |
||||
|
type="button" |
||||
|
(click)="addCreateAlarmRule()" |
||||
|
matTooltip="{{ 'alarm-rule.add-create-alarm-rule' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
{{ 'alarm-rule.add-create-alarm-rule' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,36 @@ |
|||||
|
/** |
||||
|
* 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 { |
||||
|
.create-alarm-rule { |
||||
|
border: 1px solid rgba(0, 0, 0, .12); |
||||
|
border-left-width: 4px; |
||||
|
border-radius: 4px; |
||||
|
padding: 8px; |
||||
|
min-width: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.mat-mdc-form-field.severity { |
||||
|
.mat-mdc-form-field-infix { |
||||
|
width: 160px; |
||||
|
} |
||||
|
} |
||||
|
.button-icon { |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
min-width: 40px; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,192 @@ |
|||||
|
///
|
||||
|
/// 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, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; |
||||
|
import { |
||||
|
AbstractControl, |
||||
|
ControlValueAccessor, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormArray, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormControl, |
||||
|
UntypedFormGroup, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
import { AlarmRule } from "@shared/models/alarm-rule.models"; |
||||
|
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; |
||||
|
import { AlarmSeverityNotificationColors } from "@shared/models/notification.models"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-create-cf-alarm-rules', |
||||
|
templateUrl: './create-cf-alarm-rules.component.html', |
||||
|
styleUrls: ['./create-cf-alarm-rules.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => CreateCfAlarmRulesComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => CreateCfAlarmRulesComponent), |
||||
|
multi: true, |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class CreateCfAlarmRulesComponent implements ControlValueAccessor, OnInit, Validator, OnDestroy { |
||||
|
|
||||
|
alarmSeverities = Object.keys(AlarmSeverity); |
||||
|
alarmSeverityEnum = AlarmSeverity; |
||||
|
alarmSeverityTranslationMap = alarmSeverityTranslations; |
||||
|
|
||||
|
AlarmSeverityNotificationColors = AlarmSeverityNotificationColors; |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
arguments: Record<string, CalculatedFieldArgument>; |
||||
|
|
||||
|
createAlarmRulesFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
private usedSeverities: AlarmSeverity[] = []; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
private propagateChange = (v: any) => { }; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder) { |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.createAlarmRulesFormGroup = this.fb.group({ |
||||
|
createAlarmRules: this.fb.array([]) |
||||
|
}); |
||||
|
this.createAlarmRulesFormGroup.valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe(() => this.updateModel()); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
createAlarmRulesFormArray(): UntypedFormArray { |
||||
|
return this.createAlarmRulesFormGroup.get('createAlarmRules') as UntypedFormArray; |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.createAlarmRulesFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.createAlarmRulesFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(createAlarmRules: {[severity: string]: AlarmRule}): void { |
||||
|
const createAlarmRulesControls: Array<AbstractControl> = []; |
||||
|
if (createAlarmRules) { |
||||
|
Object.keys(createAlarmRules).forEach((severity) => { |
||||
|
const createAlarmRule = createAlarmRules[severity]; |
||||
|
if (severity === 'empty') { |
||||
|
severity = null; |
||||
|
} |
||||
|
createAlarmRulesControls.push(this.fb.group({ |
||||
|
severity: [severity, Validators.required], |
||||
|
alarmRule: [createAlarmRule, Validators.required] |
||||
|
})); |
||||
|
}); |
||||
|
} |
||||
|
this.createAlarmRulesFormGroup.setControl('createAlarmRules', this.fb.array(createAlarmRulesControls), {emitEvent: false}); |
||||
|
if (this.disabled) { |
||||
|
this.createAlarmRulesFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.createAlarmRulesFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
this.updateUsedSeverities(); |
||||
|
if (!this.disabled && !this.createAlarmRulesFormGroup.valid) { |
||||
|
this.updateModel(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public removeCreateAlarmRule(index: number) { |
||||
|
(this.createAlarmRulesFormGroup.get('createAlarmRules') as UntypedFormArray).removeAt(index); |
||||
|
} |
||||
|
|
||||
|
public addCreateAlarmRule() { |
||||
|
const createAlarmRulesArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as UntypedFormArray; |
||||
|
createAlarmRulesArray.push(this.fb.group({ |
||||
|
severity: [this.getFirstUnusedSeverity(), Validators.required], |
||||
|
alarmRule: [null, Validators.required] |
||||
|
})); |
||||
|
this.createAlarmRulesFormGroup.updateValueAndValidity(); |
||||
|
if (!this.createAlarmRulesFormGroup.valid) { |
||||
|
this.updateModel(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private getFirstUnusedSeverity(): AlarmSeverity { |
||||
|
for (const severityKey of Object.keys(AlarmSeverity)) { |
||||
|
const severity = AlarmSeverity[severityKey]; |
||||
|
if (this.usedSeverities.indexOf(severity) === -1) { |
||||
|
return severity; |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public validate(c: UntypedFormControl) { |
||||
|
return (this.createAlarmRulesFormGroup.valid) ? null : { |
||||
|
createAlarmRules: { |
||||
|
valid: false, |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public isDisabledSeverity(severity: AlarmSeverity, index: number): boolean { |
||||
|
const usedIndex = this.usedSeverities.indexOf(severity); |
||||
|
return usedIndex > -1 && usedIndex !== index; |
||||
|
} |
||||
|
|
||||
|
private updateUsedSeverities() { |
||||
|
this.usedSeverities = []; |
||||
|
const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value; |
||||
|
value.forEach((rule, index) => { |
||||
|
this.usedSeverities[index] = AlarmSeverity[rule.severity]; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value; |
||||
|
const createAlarmRules: {[severity: string]: AlarmRule} = {}; |
||||
|
value.forEach(v => createAlarmRules[v.severity] = v.alarmRule); |
||||
|
this.updateUsedSeverities(); |
||||
|
this.propagateChange(createAlarmRules); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,59 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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]="complexFilterFormGroup" (ngSubmit)="save()" style="width: 1200px;"> |
||||
|
<mat-toolbar color="primary"> |
||||
|
<h2 translate>filter.complex-filter</h2> |
||||
|
<span class="flex-1"></span> |
||||
|
<button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<div mat-dialog-content> |
||||
|
<fieldset [disabled]="isLoading$ | async" class="flex flex-col"> |
||||
|
<mat-form-field class="mat-block" appearance="outline"> |
||||
|
<mat-label translate>filter.operation.operation</mat-label> |
||||
|
<mat-select required formControlName="operation"> |
||||
|
<mat-option *ngFor="let operation of complexOperations" [value]="operation"> |
||||
|
{{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<tb-alarm-rule-filter-predicate-list [valueType]="data.valueType" |
||||
|
[arguments]="arguments" |
||||
|
[operation]="complexFilterFormGroup.get('operation').value" |
||||
|
formControlName="predicates"> |
||||
|
</tb-alarm-rule-filter-predicate-list> |
||||
|
</fieldset> |
||||
|
</div> |
||||
|
<div mat-dialog-actions class="flex items-center justify-end"> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" |
||||
|
cdkFocusInitial> |
||||
|
{{'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty"> |
||||
|
{{ (isAdd ? 'action.add' : 'action.update') | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,111 @@ |
|||||
|
///
|
||||
|
/// 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, OnInit, SkipSelf } from '@angular/core'; |
||||
|
import { ErrorStateMatcher } from '@angular/material/core'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { |
||||
|
FormGroup, |
||||
|
FormGroupDirective, |
||||
|
NgForm, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormControl, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { DialogComponent } from '@app/shared/components/dialog.component'; |
||||
|
import { |
||||
|
ComplexOperation, |
||||
|
complexOperationTranslationMap, |
||||
|
EntityKeyValueType, |
||||
|
FilterPredicateType |
||||
|
} from '@shared/models/query/query.models'; |
||||
|
import { ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; |
||||
|
import { FormControlsFrom } from "@shared/models/tenant.model"; |
||||
|
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; |
||||
|
|
||||
|
export interface AlarmRuleComplexFilterPredicateDialogData { |
||||
|
complexPredicate: ComplexAlarmRuleFilterPredicate; |
||||
|
isAdd: boolean; |
||||
|
valueType: EntityKeyValueType; |
||||
|
arguments: Record<string, CalculatedFieldArgument>; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-alarm-rule-complex-filter-predicate-dialog', |
||||
|
templateUrl: './alarm-rule-complex-filter-predicate-dialog.component.html', |
||||
|
providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleComplexFilterPredicateDialogComponent}], |
||||
|
styleUrls: [] |
||||
|
}) |
||||
|
|
||||
|
export class AlarmRuleComplexFilterPredicateDialogComponent extends |
||||
|
DialogComponent<AlarmRuleComplexFilterPredicateDialogComponent, ComplexAlarmRuleFilterPredicate> |
||||
|
implements OnInit, ErrorStateMatcher { |
||||
|
|
||||
|
complexFilterFormGroup: FormGroup<FormControlsFrom<ComplexAlarmRuleFilterPredicate>>; |
||||
|
|
||||
|
complexOperations = Object.keys(ComplexOperation); |
||||
|
complexOperationEnum = ComplexOperation; |
||||
|
complexOperationTranslations = complexOperationTranslationMap; |
||||
|
|
||||
|
isAdd: boolean; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
arguments = this.data.arguments; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleComplexFilterPredicateDialogData, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<AlarmRuleComplexFilterPredicateDialogComponent, ComplexAlarmRuleFilterPredicate>, |
||||
|
private fb: UntypedFormBuilder) { |
||||
|
super(store, router, dialogRef); |
||||
|
|
||||
|
this.isAdd = this.data.isAdd; |
||||
|
|
||||
|
this.complexFilterFormGroup = this.fb.group( |
||||
|
{ |
||||
|
operation: [this.data.complexPredicate.operation, [Validators.required]], |
||||
|
predicates: [this.data.complexPredicate.predicates, [Validators.required]] |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
||||
|
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
||||
|
const customErrorState = !!(control && control.invalid && this.submitted); |
||||
|
return originalErrorState || customErrorState; |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
this.submitted = true; |
||||
|
if (this.complexFilterFormGroup.valid) { |
||||
|
const predicate: ComplexAlarmRuleFilterPredicate = this.complexFilterFormGroup.value as ComplexAlarmRuleFilterPredicate; |
||||
|
predicate.type = FilterPredicateType.COMPLEX; |
||||
|
this.dialogRef.close(predicate); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,101 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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]="filterFormGroup" (ngSubmit)="save()" style="width: 1200px;"> |
||||
|
<mat-toolbar color="primary"> |
||||
|
<h2>{{(data.isAdd ? 'alarm-rule.add-filter' : ('alarm-rule.edit-filter')) | translate}}</h2> |
||||
|
<span class="flex-1"></span> |
||||
|
<button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<div mat-dialog-content> |
||||
|
<fieldset [disabled]="isLoading$ | async" class="tb-form-panel no-border no-padding"> |
||||
|
<section class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title">{{ 'alarm-rule.general' | translate }}</div> |
||||
|
<div class="flex flex-row gap-2 xs:flex-col"> |
||||
|
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-label translate>alarm-rule.value-argument</mat-label> |
||||
|
<mat-select formControlName="argument" placeholder="{{ 'action.set' | translate }}"> |
||||
|
@for (argument of argumentsList; track argument) { |
||||
|
<mat-option [value]="argument" [disabled]="argumentInUse(argument)" >{{ argument }}</mat-option> |
||||
|
} |
||||
|
</mat-select> |
||||
|
@if (filterFormGroup.get('argument').touched && filterFormGroup.get('argument').hasError('required')) { |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="'calculated-fields.hint.argument-name-required' | translate" |
||||
|
class="tb-error !block"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
} |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="flex-1 tb-value-type" appearance="outline" subscriptSizing="dynamic" hideRequiredMarker> |
||||
|
<mat-label translate>filter.value-type.value-type</mat-label> |
||||
|
<mat-select formControlName="valueType"> |
||||
|
<mat-select-trigger> |
||||
|
<mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(filterFormGroup.get('valueType').value)?.icon }}"></mat-icon> |
||||
|
<span>{{ entityKeyValueTypes.get(filterFormGroup.get('valueType').value)?.name | translate }}</span> |
||||
|
</mat-select-trigger> |
||||
|
<mat-option *ngFor="let valueType of entityKeyValueTypesKeys" [value]="valueType"> |
||||
|
<mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).icon }}"></mat-icon> |
||||
|
<span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span> |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
<mat-error *ngIf="filterFormGroup.get('valueType').hasError('required')"> |
||||
|
{{ 'filter.value-type-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section class="tb-form-panel"> |
||||
|
<div class="flex flex-row items-center justify-between"> |
||||
|
<div class="tb-form-panel-title">{{ 'alarm-rule.filter' | 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-predicate-list [valueType]="filterFormGroup.get('valueType').value" |
||||
|
[operation]="filterFormGroup.get('operation').value" |
||||
|
[arguments]="arguments" |
||||
|
formControlName="predicates"> |
||||
|
</tb-alarm-rule-filter-predicate-list> |
||||
|
</section> |
||||
|
|
||||
|
</fieldset> |
||||
|
</div> |
||||
|
<div mat-dialog-actions class="flex items-center justify-end"> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" |
||||
|
cdkFocusInitial> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || filterFormGroup.invalid || !filterFormGroup.dirty"> |
||||
|
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,28 @@ |
|||||
|
/** |
||||
|
* 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 { |
||||
|
.mat-mdc-form-field.tb-value-type { |
||||
|
mat-select-trigger { |
||||
|
.mat-icon { |
||||
|
vertical-align: middle; |
||||
|
margin-right: 8px; |
||||
|
svg { |
||||
|
vertical-align: initial; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,153 @@ |
|||||
|
///
|
||||
|
/// 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, OnDestroy, SkipSelf } from '@angular/core'; |
||||
|
import { ErrorStateMatcher } from '@angular/material/core'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { |
||||
|
FormGroup, |
||||
|
FormGroupDirective, |
||||
|
NgForm, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormControl, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { DialogComponent } from '@app/shared/components/dialog.component'; |
||||
|
import { |
||||
|
ComplexOperation, |
||||
|
complexOperationTranslationMap, |
||||
|
EntityKeyValueType, |
||||
|
entityKeyValueTypesMap |
||||
|
} from '@shared/models/query/query.models'; |
||||
|
import { DialogService } from '@core/services/dialog.service'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
import { AlarmRuleFilter, AlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; |
||||
|
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; |
||||
|
import { FormControlsFrom } from "@shared/models/tenant.model"; |
||||
|
|
||||
|
export interface AlarmRuleFilterDialogData { |
||||
|
filter: AlarmRuleFilter; |
||||
|
isAdd: boolean; |
||||
|
arguments: Record<string, CalculatedFieldArgument>; |
||||
|
usedArguments: Array<string>; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-alarm-rule-filter-dialog', |
||||
|
templateUrl: './alarm-rule-filter-dialog.component.html', |
||||
|
providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleFilterDialogComponent}], |
||||
|
styleUrls: ['./alarm-rule-filter-dialog.component.scss'] |
||||
|
}) |
||||
|
export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFilterDialogComponent, AlarmRuleFilter> |
||||
|
implements OnDestroy, ErrorStateMatcher { |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
filterFormGroup: FormGroup<FormControlsFrom<AlarmRuleFilter>>; |
||||
|
|
||||
|
entityKeyValueTypesKeys = Object.keys(EntityKeyValueType); |
||||
|
|
||||
|
entityKeyValueTypeEnum = EntityKeyValueType; |
||||
|
|
||||
|
entityKeyValueTypes = entityKeyValueTypesMap; |
||||
|
|
||||
|
complexOperationTranslationMap = complexOperationTranslationMap; |
||||
|
|
||||
|
ComplexOperation = ComplexOperation; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
searchText = ''; |
||||
|
|
||||
|
arguments = this.data.arguments; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleFilterDialogData, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<AlarmRuleFilterDialogComponent, AlarmRuleFilter>, |
||||
|
private dialogs: DialogService, |
||||
|
private translate: TranslateService, |
||||
|
private fb: UntypedFormBuilder) { |
||||
|
super(store, router, dialogRef); |
||||
|
|
||||
|
this.filterFormGroup = this.fb.group( |
||||
|
{ |
||||
|
argument: [this.data.filter.argument, [Validators.required]], |
||||
|
valueType: [this.data.filter.valueType ?? EntityKeyValueType.STRING, [Validators.required]], |
||||
|
predicates: [this.data.filter.predicates, [Validators.required]], |
||||
|
operation: [this.data.filter.operation ?? ComplexOperation.AND] |
||||
|
} |
||||
|
); |
||||
|
this.filterFormGroup.get('valueType').valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe((valueType: EntityKeyValueType) => { |
||||
|
const prevValueType: EntityKeyValueType = this.filterFormGroup.value.valueType; |
||||
|
const predicates: AlarmRuleFilterPredicate[] = this.filterFormGroup.get('predicates').value; |
||||
|
if (prevValueType && prevValueType !== valueType) { |
||||
|
if (predicates && predicates.length) { |
||||
|
this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'), |
||||
|
this.translate.instant('filter.key-value-type-change-message')).subscribe( |
||||
|
(result) => { |
||||
|
if (result) { |
||||
|
this.filterFormGroup.get('predicates').setValue([]); |
||||
|
} else { |
||||
|
this.filterFormGroup.get('valueType').setValue(prevValueType, {emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
super.ngOnDestroy(); |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
argumentInUse(argument: string): boolean { |
||||
|
return this.data.usedArguments.includes(argument); |
||||
|
} |
||||
|
|
||||
|
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
||||
|
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
||||
|
const customErrorState = !!(control && control.invalid && this.submitted); |
||||
|
return originalErrorState || customErrorState; |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
this.submitted = true; |
||||
|
if (this.filterFormGroup.valid) { |
||||
|
const keyFilter: AlarmRuleFilter = this.filterFormGroup.getRawValue(); |
||||
|
this.dialogRef.close(keyFilter); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
get argumentsList(): Array<string> { |
||||
|
return this.arguments ? Object.keys(this.arguments) : []; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,81 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<section class="tb-form-panel no-padding no-gap stroked" [formGroup]="filterListFormGroup"> |
||||
|
<div class="flex flex-row"> |
||||
|
<span class="max-w-8% flex-full"></span> |
||||
|
<div class="flex max-w-92% flex-full flex-row items-center justify-start"> |
||||
|
<label translate class="filter-title flex-1">alarm-rule.argument-name</label> |
||||
|
<label translate class="filter-title flex-1">alarm-rule.value-type</label> |
||||
|
<span style="min-width: 96px;"> </span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<mat-divider></mat-divider> |
||||
|
<div class="filter-list"> |
||||
|
@for (filterControl of filtersFormArray.controls; track filterControl; let index = $index) { |
||||
|
<div class="flex flex-row items-stretch justify-start" style="max-height: 76px;" |
||||
|
formArrayName="filters" |
||||
|
[class.filter-list-divider]="index"> |
||||
|
<div class="filters-operation max-w-8% flex-full"> |
||||
|
@if ($index) { |
||||
|
<div class="filters-operation-container"> |
||||
|
<span class="filters-operation-label">{{ complexOperationTranslationMap.get(operation) | translate }}</span> |
||||
|
</div> |
||||
|
} |
||||
|
</div> |
||||
|
<div class="flex max-w-92% flex-full flex-col"> |
||||
|
<div class="flex flex-row items-center justify-start"> |
||||
|
<div class="flex-1">{{ filterControl.value?.argument }}</div> |
||||
|
<div class="flex-1">{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.predicates[0]?.type) | translate }}</div> |
||||
|
<button mat-icon-button color="primary" |
||||
|
type="button" |
||||
|
(click)="editFilter(index)" |
||||
|
matTooltip="{{ 'filter.edit-key-filter' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>{{'edit'}}</mat-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button color="primary" |
||||
|
type="button" |
||||
|
(click)="removeFilter(index)" |
||||
|
matTooltip="{{ 'filter.remove-key-filter' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
@if (index) { |
||||
|
<mat-divider></mat-divider> |
||||
|
} |
||||
|
</div> |
||||
|
} |
||||
|
<span [class.!hidden]="!!filtersFormArray.length" |
||||
|
class="no-data-found flex items-center justify-center" |
||||
|
translate> |
||||
|
filter.no-key-filters |
||||
|
</span> |
||||
|
</div> |
||||
|
</section> |
||||
|
<div style="margin-top: 16px;"> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
(click)="addFilter()" |
||||
|
type="button" |
||||
|
matTooltip="{{ 'alarm-rule.add-filter' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
{{ 'alarm-rule.add-filter' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
@ -0,0 +1,50 @@ |
|||||
|
/** |
||||
|
* 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 { |
||||
|
.filter-title { |
||||
|
padding: 12px 0; |
||||
|
font-size: 14px; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
.filter-list { |
||||
|
overflow: auto; |
||||
|
max-height: 300px; |
||||
|
.no-data-found { |
||||
|
height: 50px; |
||||
|
} |
||||
|
|
||||
|
&-divider { |
||||
|
border-top: 1px solid rgba(0, 0, 0, 0.12); |
||||
|
} |
||||
|
} |
||||
|
.filters-operation { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
margin-top: -14px; |
||||
|
&-container { |
||||
|
background-color: white; |
||||
|
} |
||||
|
&-label { |
||||
|
font-weight: 500; |
||||
|
color: #00695C; |
||||
|
padding: 0 8px; |
||||
|
border-radius: 4px; |
||||
|
border: 1px solid rgba(#00695C, 0.32); |
||||
|
background-color: rgba(#00695C, 0.04); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,195 @@ |
|||||
|
///
|
||||
|
/// 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, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; |
||||
|
import { |
||||
|
AbstractControl, |
||||
|
ControlValueAccessor, |
||||
|
FormControl, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormArray, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormGroup, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { Observable, Subject } from 'rxjs'; |
||||
|
import { |
||||
|
ComplexOperation, |
||||
|
complexOperationTranslationMap, |
||||
|
EntityKeyValueType |
||||
|
} from '@shared/models/query/query.models'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { deepClone } from '@core/utils'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
import { |
||||
|
AlarmRuleFilterDialogComponent, |
||||
|
AlarmRuleFilterDialogData |
||||
|
} from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component"; |
||||
|
import { AlarmRuleFilter, FilterPredicateTypeTranslationMap } from "@shared/models/alarm-rule.models"; |
||||
|
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; |
||||
|
import { UtilsService } from "@core/services/utils.service"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-alarm-rule-filter-list', |
||||
|
templateUrl: './alarm-rule-filter-list.component.html', |
||||
|
styleUrls: ['./alarm-rule-filter-list.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => AlarmRuleFilterListComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => AlarmRuleFilterListComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class AlarmRuleFilterListComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { |
||||
|
|
||||
|
@Input() |
||||
|
arguments: Record<string, CalculatedFieldArgument>; |
||||
|
|
||||
|
@Input() operation: ComplexOperation = ComplexOperation.AND; |
||||
|
|
||||
|
filterListFormGroup: UntypedFormGroup; |
||||
|
filtersControl: FormControl; |
||||
|
|
||||
|
complexOperationTranslationMap = complexOperationTranslationMap; |
||||
|
FilterPredicateTypeTranslationMap = FilterPredicateTypeTranslationMap |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
private propagateChange = null; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private utils: UtilsService, |
||||
|
private dialog: MatDialog) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.filterListFormGroup = this.fb.group({ |
||||
|
filters: this.fb.array([]) |
||||
|
}); |
||||
|
this.filtersControl = this.fb.control(null); |
||||
|
|
||||
|
this.filterListFormGroup.valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe(() => this.updateModel()); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
get filtersFormArray(): UntypedFormArray { |
||||
|
return this.filterListFormGroup.get('filters') as UntypedFormArray; |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.filterListFormGroup.valid && this.filtersControl.valid && this.filterListFormGroup.get('filters').value?.length ? null : { |
||||
|
filterList: {valid: false} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
writeValue(filters: Array<AlarmRuleFilter>): void { |
||||
|
if (filters?.length === this.filtersFormArray?.length) { |
||||
|
this.filtersFormArray.patchValue(filters, {emitEvent: false}); |
||||
|
} else { |
||||
|
const keyFilterControls: Array<AbstractControl> = []; |
||||
|
if (filters) { |
||||
|
for (const filter of filters) { |
||||
|
keyFilterControls.push(this.fb.control(filter, [Validators.required])); |
||||
|
} |
||||
|
} |
||||
|
this.filterListFormGroup.setControl('filters', this.fb.array(keyFilterControls), {emitEvent: false}); |
||||
|
} |
||||
|
this.filtersControl.patchValue(filters, {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
public removeFilter(index: number) { |
||||
|
(this.filterListFormGroup.get('filters') as UntypedFormArray).removeAt(index); |
||||
|
} |
||||
|
|
||||
|
public addFilter() { |
||||
|
const filtersFormArray = this.filterListFormGroup.get('filters') as UntypedFormArray; |
||||
|
this.openFilterDialog(null).subscribe(result => { |
||||
|
if (result) { |
||||
|
filtersFormArray.push(this.fb.control(result, [Validators.required])); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public editFilter(index: number) { |
||||
|
const filter: AlarmRuleFilter = |
||||
|
(this.filterListFormGroup.get('filters') as UntypedFormArray).at(index).value; |
||||
|
this.openFilterDialog(filter).subscribe(result => { |
||||
|
if (result) { |
||||
|
(this.filterListFormGroup.get('filters') as UntypedFormArray).at(index).patchValue(result); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private openFilterDialog(filter?: AlarmRuleFilter): Observable<AlarmRuleFilter> { |
||||
|
const isAdd = !filter; |
||||
|
if (!filter) { |
||||
|
filter = { |
||||
|
argument: null, |
||||
|
valueType: EntityKeyValueType.STRING, |
||||
|
operation: ComplexOperation.AND, |
||||
|
predicates: [] |
||||
|
}; |
||||
|
} |
||||
|
return this.dialog.open<AlarmRuleFilterDialogComponent, AlarmRuleFilterDialogData, |
||||
|
AlarmRuleFilter>(AlarmRuleFilterDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
filter: filter ? deepClone(filter) : null, |
||||
|
isAdd, |
||||
|
arguments: this.arguments, |
||||
|
usedArguments: this.getUsedArguments |
||||
|
} |
||||
|
}).afterClosed(); |
||||
|
} |
||||
|
|
||||
|
get getUsedArguments(): Array<string> { |
||||
|
const filters = this.filterListFormGroup.get('filters').value ?? []; |
||||
|
return filters.length ? filters.map((filter: AlarmRuleFilter) => filter.argument) : filters; |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
const filters: Array<AlarmRuleFilter> = this.filterListFormGroup.getRawValue().filters; |
||||
|
this.filtersControl.patchValue(filters, {emitEvent: false}); |
||||
|
if (filters.length) { |
||||
|
this.propagateChange(filters); |
||||
|
} else { |
||||
|
this.propagateChange(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,84 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<section class="flex flex-col" [formGroup]="filterListFormGroup"> |
||||
|
<div class="tb-form-panel no-padding no-gap stroked"> |
||||
|
<div class="flex flex-row"> |
||||
|
<div class="flex flex-full flex-row items-center justify-start gap-2"> |
||||
|
<div class="flex flex-1 flex-row gap-4 pl-2"> |
||||
|
<label translate class="filter-title max-w-30% flex-full">alarm-rule.operation</label> |
||||
|
<label translate class="filter-title max-w-20% flex-full xs:hidden">alarm-rule.value-source</label> |
||||
|
<label translate class="filter-title max-w-50% flex-full">alarm-rule.value</label> |
||||
|
</div> |
||||
|
<span [class.!hidden]="disabled" style="min-width: 40px;"> </span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<mat-divider style="padding-bottom: 5px;"></mat-divider> |
||||
|
<div class="predicate-list"> |
||||
|
@for (predicateControl of predicatesFormArray.controls; track predicateControl; let index = $index) { |
||||
|
<div class="flex flex-row items-stretch justify-start relative" |
||||
|
[class.key-filter-list-divider]="$index" |
||||
|
formArrayName="predicates"> |
||||
|
@if (index) { |
||||
|
<div class="filters-operation-container"> |
||||
|
<span class="filters-operation-label">{{ complexOperationTranslations.get(operation) | translate }}</span> |
||||
|
</div> |
||||
|
} |
||||
|
<div class="flex flex-full flex-col"> |
||||
|
<div class="flex flex-1 flex-row items-center justify-start"> |
||||
|
<tb-alarm-rule-filter-predicate |
||||
|
class="flex-1" |
||||
|
[arguments]="arguments" |
||||
|
[valueType]="valueType" |
||||
|
[formControl]="predicateControl"> |
||||
|
</tb-alarm-rule-filter-predicate> |
||||
|
<button mat-icon-button color="primary" |
||||
|
[class.!hidden]="disabled" |
||||
|
type="button" |
||||
|
(click)="removePredicate($index)" |
||||
|
matTooltip="{{ 'filter.remove-filter' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
<span [class.!hidden]="!!predicatesFormArray.length" |
||||
|
[class.disabled]="disabled" |
||||
|
class="no-data-found flex items-center justify-center" translate>filter.no-filters</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div style="margin-top: 16px;" class="flex flex-row gap-2"> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
[class.!hidden]="disabled" |
||||
|
(click)="addPredicate(false)" |
||||
|
type="button" |
||||
|
matTooltip="{{ 'filter.add-filter' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
{{ 'action.add' | translate }} |
||||
|
</button> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
[class.!hidden]="disabled" |
||||
|
(click)="addPredicate(true)" |
||||
|
type="button" |
||||
|
matTooltip="{{ 'filter.add-complex-filter' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
{{ 'filter.add-complex' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</section> |
||||
@ -0,0 +1,50 @@ |
|||||
|
/** |
||||
|
* 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 { |
||||
|
.filter-title { |
||||
|
padding: 12px 8px; |
||||
|
font-size: 14px; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
.predicate-list { |
||||
|
.no-data-found { |
||||
|
height: 50px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.key-filter-list-divider { |
||||
|
border-top: 1px solid rgba(0, 0, 0, 0.12); |
||||
|
} |
||||
|
.filters-operation { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
margin-top: -14px; |
||||
|
&-container { |
||||
|
position: absolute; |
||||
|
top: -12px; |
||||
|
left: 10px; |
||||
|
background-color: white; |
||||
|
} |
||||
|
&-label { |
||||
|
font-weight: 500; |
||||
|
color: #00695C; |
||||
|
padding: 0 8px; |
||||
|
border-radius: 4px; |
||||
|
border: 1px solid rgba(#00695C, 0.32); |
||||
|
background-color: rgba(#00695C, 0.04); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,239 @@ |
|||||
|
///
|
||||
|
/// 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, forwardRef, Inject, Input, OnDestroy, OnInit } from '@angular/core'; |
||||
|
import { |
||||
|
AbstractControl, |
||||
|
ControlValueAccessor, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormArray, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormGroup, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { Observable, of, Subject } from 'rxjs'; |
||||
|
import { |
||||
|
BooleanOperation, |
||||
|
ComplexOperation, |
||||
|
complexOperationTranslationMap, |
||||
|
EntityKeyValueType, |
||||
|
entityKeyValueTypeToFilterPredicateType, |
||||
|
FilterPredicateType, |
||||
|
NumericOperation, |
||||
|
StringOperation |
||||
|
} from '@shared/models/query/query.models'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { map, takeUntil } from 'rxjs/operators'; |
||||
|
import { ComponentType } from '@angular/cdk/portal'; |
||||
|
import { COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN } from '@home/components/tokens'; |
||||
|
import { |
||||
|
AlarmRuleComplexFilterPredicateDialogComponent, |
||||
|
AlarmRuleComplexFilterPredicateDialogData |
||||
|
} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component"; |
||||
|
import { |
||||
|
AlarmRuleFilterPredicate, |
||||
|
AlarmRulePredicateInfo, |
||||
|
ComplexAlarmRuleFilterPredicate |
||||
|
} from "@shared/models/alarm-rule.models"; |
||||
|
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-alarm-rule-filter-predicate-list', |
||||
|
templateUrl: './alarm-rule-filter-predicate-list.component.html', |
||||
|
styleUrls: ['./alarm-rule-filter-predicate-list.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => AlarmRuleFilterPredicateListComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => AlarmRuleFilterPredicateListComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class AlarmRuleFilterPredicateListComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { |
||||
|
|
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
@Input() valueType: EntityKeyValueType; |
||||
|
|
||||
|
@Input() operation: ComplexOperation = ComplexOperation.AND; |
||||
|
|
||||
|
@Input() arguments: Record<string, CalculatedFieldArgument>; |
||||
|
|
||||
|
filterListFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
valueTypeEnum = EntityKeyValueType; |
||||
|
|
||||
|
complexOperationTranslations = complexOperationTranslationMap; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
private propagateChange = null; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
@Inject(COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN) private complexFilterPredicateDialogComponent: ComponentType<any>, |
||||
|
private dialog: MatDialog) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.filterListFormGroup = this.fb.group({ |
||||
|
predicates: this.fb.array([]) |
||||
|
}); |
||||
|
this.filterListFormGroup.valueChanges.pipe( |
||||
|
takeUntil(this.destroy$) |
||||
|
).subscribe(() => this.updateModel()); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
get predicatesFormArray(): UntypedFormArray { |
||||
|
return this.filterListFormGroup.get('predicates') as UntypedFormArray; |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState?(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.filterListFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.filterListFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
validate(control: AbstractControl): ValidationErrors | null { |
||||
|
return this.filterListFormGroup.valid ? null : { |
||||
|
filterList: {valid: false} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
writeValue(predicates: Array<AlarmRulePredicateInfo>): void { |
||||
|
if (predicates?.length === this.predicatesFormArray.length) { |
||||
|
this.predicatesFormArray.patchValue(predicates, {emitEvent: false}); |
||||
|
} else { |
||||
|
const predicateControls: Array<AbstractControl> = []; |
||||
|
if (predicates) { |
||||
|
for (const predicate of predicates) { |
||||
|
predicateControls.push(this.fb.control(predicate, [Validators.required])); |
||||
|
} |
||||
|
} |
||||
|
this.filterListFormGroup.setControl('predicates', this.fb.array(predicateControls), {emitEvent: false}); |
||||
|
if (this.disabled) { |
||||
|
this.filterListFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.filterListFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public removePredicate(index: number) { |
||||
|
this.predicatesFormArray.removeAt(index); |
||||
|
} |
||||
|
|
||||
|
public addPredicate(complex: boolean) { |
||||
|
const predicatesFormArray = this.filterListFormGroup.get('predicates') as UntypedFormArray; |
||||
|
const predicate = this.createDefaultFilterPredicate(this.valueType, complex); |
||||
|
let observable: Observable<AlarmRuleFilterPredicate>; |
||||
|
if (complex) { |
||||
|
observable = this.openComplexFilterDialog(predicate as ComplexAlarmRuleFilterPredicate); |
||||
|
} else { |
||||
|
observable = of(predicate); |
||||
|
} |
||||
|
observable.subscribe((result) => { |
||||
|
if (result) { |
||||
|
predicatesFormArray.push(this.fb.control(result, [Validators.required])); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private createDefaultFilterPredicate(valueType: EntityKeyValueType, complex: boolean): AlarmRuleFilterPredicate { |
||||
|
const predicate = { |
||||
|
type: complex ? FilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType) |
||||
|
} as AlarmRuleFilterPredicate; |
||||
|
switch (predicate.type) { |
||||
|
case FilterPredicateType.STRING: |
||||
|
predicate.operation = StringOperation.STARTS_WITH; |
||||
|
predicate.value = { |
||||
|
staticValue: '' |
||||
|
}; |
||||
|
predicate.ignoreCase = false; |
||||
|
break; |
||||
|
case FilterPredicateType.NUMERIC: |
||||
|
predicate.operation = NumericOperation.EQUAL; |
||||
|
predicate.value = { |
||||
|
staticValue: valueType === EntityKeyValueType.DATE_TIME ? Date.now() : 0 |
||||
|
}; |
||||
|
break; |
||||
|
case FilterPredicateType.BOOLEAN: |
||||
|
predicate.operation = BooleanOperation.EQUAL; |
||||
|
predicate.value = { |
||||
|
staticValue: false |
||||
|
}; |
||||
|
break; |
||||
|
case FilterPredicateType.COMPLEX: |
||||
|
predicate.operation = ComplexOperation.AND; |
||||
|
predicate.predicates = []; |
||||
|
break; |
||||
|
} |
||||
|
return predicate; |
||||
|
} |
||||
|
|
||||
|
private openComplexFilterDialog(predicate: ComplexAlarmRuleFilterPredicate): Observable<ComplexAlarmRuleFilterPredicate> { |
||||
|
return this.dialog.open<AlarmRuleComplexFilterPredicateDialogComponent, AlarmRuleComplexFilterPredicateDialogData, |
||||
|
ComplexAlarmRuleFilterPredicate>(AlarmRuleComplexFilterPredicateDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
complexPredicate: predicate as ComplexAlarmRuleFilterPredicate, |
||||
|
valueType: this.valueType, |
||||
|
isAdd: true, |
||||
|
arguments: this.arguments, |
||||
|
} |
||||
|
}).afterClosed().pipe( |
||||
|
map((result) => { |
||||
|
if (result) { |
||||
|
predicate = result; |
||||
|
return predicate; |
||||
|
} else { |
||||
|
return null; |
||||
|
} |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
const predicates: Array<AlarmRulePredicateInfo> = this.filterListFormGroup.getRawValue().predicates; |
||||
|
if (predicates.length) { |
||||
|
this.propagateChange(predicates); |
||||
|
} else { |
||||
|
this.propagateChange(null); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-form-panel no-border no-gap no-padding" [formGroup]="filterPredicateValueFormGroup"> |
||||
|
<div class="tb-form-row no-padding no-border column-xs"> |
||||
|
<mat-form-field hideRequiredMarker class="flex-1 w-full max-w-30% xs:max-w-full" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select [ngModel]="mode" |
||||
|
(ngModelChange)="onModeChange($event)" |
||||
|
[ngModelOptions]="{ standalone: true }" |
||||
|
placeholder="{{'filter.dynamic-source-type' | translate}}"> |
||||
|
<mat-option [value]="'static'"> |
||||
|
{{'alarm-rule.static' | translate}} |
||||
|
</mat-option> |
||||
|
<mat-option [value]="'dynamic'"> |
||||
|
{{'alarm-rule.dynamic' | translate}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
@if (mode === 'static') { |
||||
|
@switch (valueType) { |
||||
|
@case (valueTypeEnum.STRING) { |
||||
|
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="staticValue" placeholder="{{'filter.value' | translate}}"> |
||||
|
</mat-form-field> |
||||
|
} |
||||
|
@case (valueTypeEnum.NUMERIC) { |
||||
|
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input required type="number" matInput formControlName="staticValue" |
||||
|
placeholder="{{'filter.value' | translate}}"> |
||||
|
</mat-form-field> |
||||
|
} |
||||
|
@case (valueTypeEnum.BOOLEAN) { |
||||
|
<mat-checkbox formControlName="staticValue"> |
||||
|
{{ (filterPredicateValueFormGroup.get('staticValue').value ? 'value.true' : 'value.false') | translate }} |
||||
|
</mat-checkbox> |
||||
|
} |
||||
|
@case (valueTypeEnum.DATE_TIME) { |
||||
|
<tb-datetime formControlName="staticValue" |
||||
|
class="flex-1" |
||||
|
appearance="outline" |
||||
|
subscriptSizing="dynamic" |
||||
|
fieldClass="flex-1" |
||||
|
dateText="{{ 'filter.date' | translate }}" |
||||
|
required [showLabel]="false"></tb-datetime> |
||||
|
} |
||||
|
} |
||||
|
} @else { |
||||
|
<mat-form-field class="flex-1 w-full" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="dynamicValueArgument" placeholder="{{ 'action.set' | translate }}"> |
||||
|
@for (argument of argumentsList; track argument) { |
||||
|
<mat-option [value]="argument">{{ argument }}</mat-option> |
||||
|
} |
||||
|
</mat-select> |
||||
|
@if (filterPredicateValueFormGroup.get('dynamicValueArgument').touched && filterPredicateValueFormGroup.get('dynamicValueArgument').hasError('required')) { |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="'calculated-fields.hint.argument-name-required' | translate" |
||||
|
class="tb-error !block"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
} |
||||
|
</mat-form-field> |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,159 @@ |
|||||
|
///
|
||||
|
/// 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, forwardRef, Input, OnInit } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormGroup, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormBuilder, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
ValidatorFn, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { EntityKeyValueType } from '@shared/models/query/query.models'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; |
||||
|
import { AlarmRuleValue } from "@shared/models/alarm-rule.models"; |
||||
|
import { isDefinedAndNotNull } from "@core/utils"; |
||||
|
import { FormControlsFrom } from "@shared/models/tenant.model"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-alarm-rule-filter-predicate-value', |
||||
|
templateUrl: './alarm-rule-filter-predicate-value.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => AlarmRuleFilterPredicateValueComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => AlarmRuleFilterPredicateValueComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class AlarmRuleFilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit { |
||||
|
|
||||
|
@Input() |
||||
|
arguments: Record<string, CalculatedFieldArgument>; |
||||
|
|
||||
|
@Input() |
||||
|
valueType: EntityKeyValueType; |
||||
|
|
||||
|
valueTypeEnum = EntityKeyValueType; |
||||
|
|
||||
|
filterPredicateValueFormGroup: FormGroup<FormControlsFrom<AlarmRuleValue<string | number | boolean>>>; |
||||
|
|
||||
|
mode: 'static' | 'dynamic' = 'static'; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
private propagateChangePending = false; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private destroyRef: DestroyRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
let defaultValue: string | number | boolean; |
||||
|
let defaultValueValidators: ValidatorFn[]; |
||||
|
switch (this.valueType) { |
||||
|
case EntityKeyValueType.STRING: |
||||
|
defaultValue = ''; |
||||
|
defaultValueValidators = []; |
||||
|
break; |
||||
|
case EntityKeyValueType.NUMERIC: |
||||
|
defaultValue = 0; |
||||
|
defaultValueValidators = [Validators.required]; |
||||
|
break; |
||||
|
case EntityKeyValueType.BOOLEAN: |
||||
|
defaultValue = false; |
||||
|
defaultValueValidators = []; |
||||
|
break; |
||||
|
case EntityKeyValueType.DATE_TIME: |
||||
|
defaultValue = Date.now(); |
||||
|
defaultValueValidators = [Validators.required]; |
||||
|
break; |
||||
|
} |
||||
|
this.filterPredicateValueFormGroup = this.fb.group({ |
||||
|
staticValue: [defaultValue, defaultValueValidators], |
||||
|
dynamicValueArgument: [null, Validators.required] |
||||
|
}); |
||||
|
this.filterPredicateValueFormGroup.valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateValueModeValidators(mode: 'static' | 'dynamic'): void { |
||||
|
if (mode === 'static') { |
||||
|
this.filterPredicateValueFormGroup.get('staticValue').enable({emitEvent: false}); |
||||
|
this.filterPredicateValueFormGroup.get('dynamicValueArgument').disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.filterPredicateValueFormGroup.get('staticValue').disable({emitEvent: false}); |
||||
|
this.filterPredicateValueFormGroup.get('dynamicValueArgument').enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
get argumentsList(): Array<string> { |
||||
|
return this.arguments ? Object.keys(this.arguments): []; |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
if (this.propagateChangePending) { |
||||
|
this.propagateChangePending = false; |
||||
|
setTimeout(() => { |
||||
|
this.updateModel(); |
||||
|
}, 0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onModeChange(mode: 'static' | 'dynamic') { |
||||
|
this.mode = mode; |
||||
|
this.updateValueModeValidators(mode); |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.filterPredicateValueFormGroup.valid ? null : { |
||||
|
filterPredicateValue: {valid: false} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
writeValue(predicateValue: AlarmRuleValue<string | number | boolean>): void { |
||||
|
this.propagateChangePending = false; |
||||
|
this.filterPredicateValueFormGroup.patchValue(predicateValue, {emitEvent: false}); |
||||
|
this.mode = isDefinedAndNotNull(predicateValue.dynamicValueArgument) ? 'dynamic' : 'static'; |
||||
|
this.updateValueModeValidators(this.mode); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
const predicateValue: AlarmRuleValue<string | number | boolean> = this.filterPredicateValueFormGroup.value; |
||||
|
if (this.propagateChange) { |
||||
|
this.propagateChange(predicateValue); |
||||
|
} else { |
||||
|
this.propagateChangePending = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,79 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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="flex flex-1 flex-row items-center justify-start gap-4" [formGroup]="filterPredicateFormGroup"> |
||||
|
<div class="tb-form-panel no-border no-padding-right flex-1" style="flex-direction: row"> |
||||
|
@switch (type) { |
||||
|
@case (filterPredicateType.STRING) { |
||||
|
<div class="tb-form-row no-padding no-border column-xs w-full max-w-30% xs:max-w-full flex-1/"> |
||||
|
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}"> |
||||
|
<mat-option *ngFor="let operation of stringOperations" [value]="operation"> |
||||
|
{{stringOperationTranslationMap.get(stringOperation[operation]) | translate}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-chip-listbox formControlName="ignoreCase" [hideSingleSelectionIndicator]="true"> |
||||
|
<mat-chip-option color="primary" [value]="true">{{ 'alarm-rule.ignore-case' | translate }}</mat-chip-option> |
||||
|
</mat-chip-listbox> |
||||
|
</div> |
||||
|
} |
||||
|
@case (filterPredicateType.NUMERIC) { |
||||
|
<div class="tb-form-row no-border no-padding no-border w-full max-w-30%"> |
||||
|
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}"> |
||||
|
<mat-option *ngFor="let operation of numericOperations" [value]="operation"> |
||||
|
{{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
} |
||||
|
@case (filterPredicateType.BOOLEAN) { |
||||
|
<div class="tb-form-row no-border no-padding no-border w-full max-w-30%"> |
||||
|
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}"> |
||||
|
<mat-option *ngFor="let operation of booleanOperations" [value]="operation"> |
||||
|
{{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
} |
||||
|
@case (filterPredicateType.COMPLEX) { |
||||
|
<div class="tb-form-row no-border no-padding flex-1"> |
||||
|
<button type="button" style="--mat-outlined-button-horizontal-padding: 3px 0px 12px;" |
||||
|
class="block w-full" |
||||
|
mat-stroked-button color="primary" |
||||
|
(click)="openComplexFilterDialog()"> |
||||
|
<div class="flex items-center gap-2"> |
||||
|
<span class="w-full text-start" translate>filter.complex-filter</span> |
||||
|
<mat-icon class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">edit</mat-icon> |
||||
|
</div> |
||||
|
</button> |
||||
|
</div> |
||||
|
} |
||||
|
} |
||||
|
@if (type !== filterPredicateType.COMPLEX) { |
||||
|
<tb-alarm-rule-filter-predicate-value class="flex-full" |
||||
|
[arguments]="arguments" |
||||
|
[valueType]="valueType" |
||||
|
formControlName="value"> |
||||
|
</tb-alarm-rule-filter-predicate-value> |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,158 @@ |
|||||
|
///
|
||||
|
/// 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, forwardRef, Input, OnInit } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormGroup, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormBuilder, |
||||
|
ValidationErrors, |
||||
|
Validator |
||||
|
} from '@angular/forms'; |
||||
|
import { |
||||
|
BooleanOperation, |
||||
|
booleanOperationTranslationMap, |
||||
|
EntityKeyValueType, |
||||
|
FilterPredicateType, |
||||
|
NumericOperation, |
||||
|
numericOperationTranslationMap, |
||||
|
StringOperation, |
||||
|
stringOperationTranslationMap |
||||
|
} from '@shared/models/query/query.models'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
import { AlarmRuleFilterPredicate, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; |
||||
|
import { MatDialog } from "@angular/material/dialog"; |
||||
|
import { |
||||
|
AlarmRuleComplexFilterPredicateDialogComponent, |
||||
|
AlarmRuleComplexFilterPredicateDialogData |
||||
|
} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component"; |
||||
|
import { FormControlsFrom } from "@shared/models/tenant.model"; |
||||
|
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-alarm-rule-filter-predicate', |
||||
|
templateUrl: './alarm-rule-filter-predicate.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => AlarmRuleFilterPredicateComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => AlarmRuleFilterPredicateComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { |
||||
|
|
||||
|
@Input() |
||||
|
valueType: EntityKeyValueType; |
||||
|
|
||||
|
@Input() |
||||
|
arguments: Record<string, CalculatedFieldArgument>; |
||||
|
|
||||
|
filterPredicateFormGroup: FormGroup<FormControlsFrom<AlarmRuleFilterPredicate>>; |
||||
|
|
||||
|
type: FilterPredicateType; |
||||
|
|
||||
|
filterPredicateType = FilterPredicateType; |
||||
|
|
||||
|
stringOperations = Object.keys(StringOperation); |
||||
|
stringOperation = StringOperation; |
||||
|
stringOperationTranslationMap = stringOperationTranslationMap; |
||||
|
|
||||
|
numericOperations = Object.keys(NumericOperation); |
||||
|
numericOperationEnum = NumericOperation; |
||||
|
numericOperationTranslations = numericOperationTranslationMap; |
||||
|
|
||||
|
booleanOperations = Object.keys(BooleanOperation); |
||||
|
booleanOperationEnum = BooleanOperation; |
||||
|
booleanOperationTranslations = booleanOperationTranslationMap; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private dialog: MatDialog, |
||||
|
private destroyRef: DestroyRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.filterPredicateFormGroup = this.fb.group({ |
||||
|
operation: [], |
||||
|
ignoreCase: false, |
||||
|
predicates: [], |
||||
|
value: [] |
||||
|
}); |
||||
|
this.filterPredicateFormGroup.valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.filterPredicateFormGroup.valid ? null : { |
||||
|
filterPredicate: {valid: false} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
writeValue(predicate: AlarmRuleFilterPredicate): void { |
||||
|
this.type = predicate.type; |
||||
|
this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
let predicate: AlarmRuleFilterPredicate = null; |
||||
|
if (this.filterPredicateFormGroup.valid) { |
||||
|
predicate = this.filterPredicateFormGroup.getRawValue(); |
||||
|
predicate.type = this.type; |
||||
|
} |
||||
|
this.propagateChange(predicate); |
||||
|
} |
||||
|
|
||||
|
public openComplexFilterDialog() { |
||||
|
this.dialog.open<AlarmRuleComplexFilterPredicateDialogComponent, AlarmRuleComplexFilterPredicateDialogData, |
||||
|
ComplexAlarmRuleFilterPredicate>(AlarmRuleComplexFilterPredicateDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
complexPredicate: this.filterPredicateFormGroup.getRawValue() as ComplexAlarmRuleFilterPredicate, |
||||
|
valueType: this.valueType, |
||||
|
isAdd: false, |
||||
|
arguments: this.arguments, |
||||
|
} |
||||
|
}).afterClosed().subscribe( |
||||
|
(result) => { |
||||
|
if (result) { |
||||
|
this.filterPredicateFormGroup.patchValue(result, {emitEvent: false}); |
||||
|
this.updateModel(); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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-filter-text" |
||||
|
tbTruncateWithTooltip |
||||
|
[class.required]="requiredClass" |
||||
|
[class.nowrap]="nowrap" |
||||
|
[innerHTML]="filterText"> |
||||
|
</div> |
||||
@ -0,0 +1,85 @@ |
|||||
|
/** |
||||
|
* 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 { |
||||
|
text-overflow: ellipsis; |
||||
|
overflow: hidden; |
||||
|
.tb-filter-text { |
||||
|
overflow-y: auto; |
||||
|
text-align: start; |
||||
|
&.required { |
||||
|
color: #f44336; |
||||
|
padding: 0; |
||||
|
} |
||||
|
&.nowrap { |
||||
|
white-space: nowrap; |
||||
|
text-overflow: ellipsis; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.tb-filter-text { |
||||
|
line-height: 1.8em; |
||||
|
span { |
||||
|
display: inline-block; |
||||
|
vertical-align: middle; |
||||
|
line-height: 1.4em; |
||||
|
} |
||||
|
.tb-filter-predicate { |
||||
|
padding-right: 4px; |
||||
|
padding-left: 4px; |
||||
|
} |
||||
|
.tb-filter-entity-key, .tb-filter-value, .tb-filter-dynamic-source { |
||||
|
font-weight: bold; |
||||
|
border: 1px groove rgba(0, 0, 0, .25); |
||||
|
border-radius: 4px; |
||||
|
padding-left: 4px; |
||||
|
padding-right: 4px; |
||||
|
} |
||||
|
.tb-filter-entity-key, .tb-filter-value { |
||||
|
white-space: nowrap; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
max-width: 150px; |
||||
|
} |
||||
|
.tb-filter-dynamic-source { |
||||
|
} |
||||
|
.tb-filter-entity-key { |
||||
|
color: #305680; |
||||
|
} |
||||
|
.tb-filter-value { |
||||
|
color: #ff5722; |
||||
|
} |
||||
|
.tb-filter-simple-operation { |
||||
|
font-size: 0.9em; |
||||
|
} |
||||
|
.tb-filter-complex-operation { |
||||
|
font-weight: 400; |
||||
|
font-style: italic; |
||||
|
} |
||||
|
.tb-filter-dynamic-value { |
||||
|
.tb-filter-dynamic-source, .tb-filter-value { |
||||
|
color: #0c959c; |
||||
|
} |
||||
|
} |
||||
|
.tb-filter-bracket { |
||||
|
.tb-left-bracket, .tb-right-bracket { |
||||
|
font-size: 1.2em; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,192 @@ |
|||||
|
///
|
||||
|
/// 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, Input } from '@angular/core'; |
||||
|
import { |
||||
|
booleanOperationTranslationMap, |
||||
|
ComplexOperation, |
||||
|
complexOperationTranslationMap, |
||||
|
EntityKeyValueType, |
||||
|
FilterPredicateType, |
||||
|
numericOperationTranslationMap, |
||||
|
stringOperationTranslationMap |
||||
|
} from '@shared/models/query/query.models'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { DatePipe } from '@angular/common'; |
||||
|
import { |
||||
|
AlarmRuleExpression, |
||||
|
AlarmRuleExpressionType, |
||||
|
AlarmRuleFilter, |
||||
|
AlarmRuleFilterPredicate, |
||||
|
ComplexAlarmRuleFilterPredicate |
||||
|
} from "@shared/models/alarm-rule.models"; |
||||
|
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; |
||||
|
import { coerceBoolean } from "@shared/decorators/coercion"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-alarm-rule-filter-text', |
||||
|
templateUrl: './alarm-rule-filter-text.component.html', |
||||
|
styleUrls: ['./alarm-rule-filter-text.component.scss'], |
||||
|
providers: [] |
||||
|
}) |
||||
|
export class AlarmRuleFilterTextComponent { |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
required = false; |
||||
|
|
||||
|
@Input() |
||||
|
noFilterText = this.translate.instant('filter.no-filter-text'); |
||||
|
|
||||
|
@Input() |
||||
|
addFilterPrompt = this.translate.instant('filter.add-filter-prompt'); |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
nowrap = false; |
||||
|
|
||||
|
@Input() |
||||
|
arguments: Record<string, CalculatedFieldArgument>; |
||||
|
|
||||
|
private alarmRuleExpressionValue: AlarmRuleExpression; |
||||
|
get alarmRuleExpression(): AlarmRuleExpression { |
||||
|
return this.alarmRuleExpressionValue; |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
set alarmRuleExpression(value: AlarmRuleExpression) { |
||||
|
if (value !== this.alarmRuleExpressionValue) { |
||||
|
this.alarmRuleExpressionValue = value; |
||||
|
this.updateFilterText(value); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
private specTextValue: string; |
||||
|
get specText(): string { |
||||
|
return this.specTextValue; |
||||
|
} |
||||
|
@Input() |
||||
|
set specText(value: string) { |
||||
|
if (value !== this.specTextValue) { |
||||
|
this.specTextValue = value; |
||||
|
this.updateFilterText(this.alarmRuleExpression); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
requiredClass = false; |
||||
|
|
||||
|
public filterText: string; |
||||
|
|
||||
|
constructor(private translate: TranslateService, |
||||
|
private datePipe: DatePipe) { |
||||
|
} |
||||
|
|
||||
|
private updateFilterText(value: AlarmRuleExpression) { |
||||
|
this.requiredClass = false; |
||||
|
if (value && (value.expression || value.filters)) { |
||||
|
if (value.type === AlarmRuleExpressionType.SIMPLE) { |
||||
|
this.filterText = this.keyFiltersToText(this.translate, this.datePipe, value.filters, value.operation); |
||||
|
} else { |
||||
|
this.filterText = 'function expression(ctx, ' + (this.arguments ? Object.keys(this.arguments).join(', ') : '' ) + ')'; |
||||
|
} |
||||
|
if (this.specText?.length) { |
||||
|
this.filterText = this.specText + ': ' + this.filterText; |
||||
|
} |
||||
|
} else { |
||||
|
if (this.required) { |
||||
|
this.filterText = this.addFilterPrompt; |
||||
|
this.requiredClass = true; |
||||
|
} else { |
||||
|
this.filterText = this.noFilterText; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array<AlarmRuleFilter>, operation: ComplexOperation): string { |
||||
|
const filtersText = keyFilters.map(keyFilter => |
||||
|
this.filterPredicateToText(translate, datePipe, keyFilter, keyFilter.predicates)); |
||||
|
let result: string; |
||||
|
if (filtersText.length > 1) { |
||||
|
const operationText = translate.instant(complexOperationTranslationMap.get(operation)); |
||||
|
result = filtersText.join(' <span class="tb-filter-complex-operation">' + operationText + '</span> '); |
||||
|
} else { |
||||
|
result = filtersText[0]; |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private filterPredicateToText(translate: TranslateService, |
||||
|
datePipe: DatePipe, |
||||
|
keyFilter: AlarmRuleFilter, |
||||
|
keyFilterPredicates: AlarmRuleFilterPredicate[], |
||||
|
complexOperation?: ComplexOperation): string { |
||||
|
const key = keyFilter.argument; |
||||
|
const filterOperation: ComplexOperation = complexOperation ? complexOperation : (keyFilter.operation ?? ComplexOperation.AND); |
||||
|
|
||||
|
const predicates = keyFilterPredicates.map((keyFilterPredicate: AlarmRuleFilterPredicate) => { |
||||
|
if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) { |
||||
|
const complexPredicate = keyFilterPredicate as ComplexAlarmRuleFilterPredicate; |
||||
|
const complexOperation = complexPredicate.operation ?? ComplexOperation.AND; |
||||
|
return this.filterPredicateToText(translate, datePipe, keyFilter, complexPredicate.predicates, complexOperation); |
||||
|
} else { |
||||
|
let operation: string; |
||||
|
let value: string; |
||||
|
const val = keyFilterPredicate.value; |
||||
|
const dynamicValue = val?.dynamicValueArgument?.length; |
||||
|
if (dynamicValue) { |
||||
|
value = '<span class="tb-filter-dynamic-value"><span class="tb-filter-value">' + val?.dynamicValueArgument + '</span></span>'; |
||||
|
} |
||||
|
switch (keyFilterPredicate.type) { |
||||
|
case FilterPredicateType.STRING: |
||||
|
operation = translate.instant(stringOperationTranslationMap.get(keyFilterPredicate.operation)); |
||||
|
if (keyFilterPredicate.ignoreCase) { |
||||
|
operation += ' ' + translate.instant('filter.ignore-case'); |
||||
|
} |
||||
|
if (!dynamicValue) { |
||||
|
value = `'${keyFilterPredicate.value.staticValue}'`; |
||||
|
} |
||||
|
break; |
||||
|
case FilterPredicateType.NUMERIC: |
||||
|
operation = translate.instant(numericOperationTranslationMap.get(keyFilterPredicate.operation)); |
||||
|
if (!dynamicValue) { |
||||
|
if (keyFilter.valueType === EntityKeyValueType.DATE_TIME) { |
||||
|
value = datePipe.transform(keyFilterPredicate.value.staticValue, 'yyyy-MM-dd HH:mm'); |
||||
|
} else { |
||||
|
value = keyFilterPredicate.value.staticValue + ''; |
||||
|
} |
||||
|
} |
||||
|
break; |
||||
|
case FilterPredicateType.BOOLEAN: |
||||
|
operation = translate.instant(booleanOperationTranslationMap.get(keyFilterPredicate.operation)); |
||||
|
if (!dynamicValue) { |
||||
|
value = translate.instant(keyFilterPredicate.value.staticValue ? 'value.true' : 'value.false'); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
if (!dynamicValue) { |
||||
|
value = `<span class="tb-filter-value">${value}</span>`; |
||||
|
} |
||||
|
return `<span class="tb-filter-predicate"><span class="tb-filter-entity-key">${key}</span> <span class="tb-filter-simple-operation">${operation}</span> ${value}</span>` |
||||
|
} |
||||
|
}); |
||||
|
if (predicates.length > 1) { |
||||
|
return '(' + predicates.join(` ${translate.instant(complexOperationTranslationMap.get(filterOperation))} `)+ ')'; |
||||
|
} else { |
||||
|
return predicates.toString(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,152 @@ |
|||||
|
///
|
||||
|
/// 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 { CustomTimeSchedulerItem } from "@shared/models/device.models"; |
||||
|
import { DashboardId } from "@shared/models/id/dashboard-id"; |
||||
|
import { TimeUnit } from "@shared/models/time/time.models"; |
||||
|
import { |
||||
|
BooleanOperation, |
||||
|
ComplexOperation, |
||||
|
EntityKeyValueType, |
||||
|
FilterPredicateType, |
||||
|
NumericOperation, |
||||
|
StringOperation |
||||
|
} from "@shared/models/query/query.models"; |
||||
|
|
||||
|
export enum AlarmRuleScheduleType { |
||||
|
ANY_TIME = 'ANY_TIME', |
||||
|
SPECIFIC_TIME = 'SPECIFIC_TIME', |
||||
|
CUSTOM = 'CUSTOM' |
||||
|
} |
||||
|
|
||||
|
export const AlarmRuleScheduleTypeTranslationMap = new Map<AlarmRuleScheduleType, string>( |
||||
|
[ |
||||
|
[AlarmRuleScheduleType.ANY_TIME, 'alarm-rule.schedule.any-time'], |
||||
|
[AlarmRuleScheduleType.SPECIFIC_TIME, 'alarm-rule.schedule.specific-time'], |
||||
|
[AlarmRuleScheduleType.CUSTOM, 'alarm-rule.schedule.custom'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export enum AlarmRuleConditionType { |
||||
|
SIMPLE = 'SIMPLE', |
||||
|
DURATION = 'DURATION', |
||||
|
REPEATING = 'REPEATING' |
||||
|
} |
||||
|
|
||||
|
export const AlarmRuleConditionTypeTranslationMap = new Map<AlarmRuleConditionType, string>( |
||||
|
[ |
||||
|
[AlarmRuleConditionType.SIMPLE, 'alarm-rule.conditions.simple'], |
||||
|
[AlarmRuleConditionType.DURATION, 'alarm-rule.conditions.duration'], |
||||
|
[AlarmRuleConditionType.REPEATING, 'alarm-rule.conditions.repeating'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export enum AlarmRuleExpressionType { |
||||
|
SIMPLE = 'SIMPLE', |
||||
|
TBEL = 'TBEL', |
||||
|
} |
||||
|
|
||||
|
export const FilterPredicateTypeTranslationMap = new Map<FilterPredicateType, string>( |
||||
|
[ |
||||
|
[FilterPredicateType.STRING, 'alarm-rule.filter-predicate-type.string'], |
||||
|
[FilterPredicateType.NUMERIC, 'alarm-rule.filter-predicate-type.numeric'], |
||||
|
[FilterPredicateType.BOOLEAN, 'alarm-rule.filter-predicate-type.boolean'], |
||||
|
[FilterPredicateType.COMPLEX, 'alarm-rule.filter-predicate-type.complex'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export interface AlarmRule { |
||||
|
condition: AlarmRuleCondition; |
||||
|
alarmDetails?: string; |
||||
|
dashboardId?: DashboardId; |
||||
|
} |
||||
|
|
||||
|
export interface AlarmRuleCondition { |
||||
|
type: AlarmRuleConditionType; |
||||
|
expression: AlarmRuleExpression; |
||||
|
schedule?: AlarmRuleSchedule; |
||||
|
unit?: TimeUnit; |
||||
|
value?: AlarmRuleValue<number>; |
||||
|
count?: AlarmRuleValue<number>; |
||||
|
} |
||||
|
|
||||
|
export interface AlarmRuleExpression { |
||||
|
type: AlarmRuleExpressionType; |
||||
|
expression?: string; |
||||
|
filters?: Array<AlarmRuleFilter>; |
||||
|
operation?: ComplexOperation; |
||||
|
} |
||||
|
|
||||
|
export interface AlarmRuleSchedule { |
||||
|
staticValue?: { |
||||
|
type?: AlarmRuleScheduleType; |
||||
|
timezone?: string; |
||||
|
daysOfWeek?: number[]; |
||||
|
startsOn?: number; |
||||
|
endsOn?: number; |
||||
|
items?: CustomTimeSchedulerItem[]; |
||||
|
}; |
||||
|
dynamicValueArgument?: string; |
||||
|
} |
||||
|
|
||||
|
export interface AlarmRuleFilter { |
||||
|
argument: string; |
||||
|
valueType: EntityKeyValueType; |
||||
|
operation: ComplexOperation; |
||||
|
predicates: AlarmRuleFilterPredicate[]; |
||||
|
} |
||||
|
|
||||
|
export interface AlarmRulePredicateInfo { |
||||
|
keyFilterPredicate: AlarmRuleFilterPredicate; |
||||
|
} |
||||
|
|
||||
|
export type AlarmRuleFilterPredicate = StringAlarmRuleFilterPredicate | |
||||
|
NumericAlarmRuleFilterPredicate | |
||||
|
BooleanAlarmRuleFilterPredicate | |
||||
|
ComplexAlarmRuleFilterPredicate; |
||||
|
|
||||
|
export interface AlarmRuleValue<T> { |
||||
|
dynamicValueArgument?: string; |
||||
|
staticValue?: T |
||||
|
} |
||||
|
|
||||
|
export interface StringAlarmRuleFilterPredicate { |
||||
|
type: FilterPredicateType.STRING; |
||||
|
operation: StringOperation; |
||||
|
value: AlarmRuleValue<string>; |
||||
|
ignoreCase: boolean; |
||||
|
} |
||||
|
|
||||
|
export interface NumericAlarmRuleFilterPredicate { |
||||
|
type: FilterPredicateType.NUMERIC; |
||||
|
operation: NumericOperation; |
||||
|
value: AlarmRuleValue<number>; |
||||
|
} |
||||
|
|
||||
|
export interface BooleanAlarmRuleFilterPredicate { |
||||
|
type: FilterPredicateType.BOOLEAN; |
||||
|
operation: BooleanOperation; |
||||
|
value: AlarmRuleValue<boolean>; |
||||
|
} |
||||
|
|
||||
|
export interface BaseComplexFilterPredicate<T extends AlarmRuleFilterPredicate> { |
||||
|
type: FilterPredicateType.COMPLEX; |
||||
|
operation: ComplexOperation; |
||||
|
predicates: Array<T>; |
||||
|
} |
||||
|
|
||||
|
export type ComplexAlarmRuleFilterPredicate = BaseComplexFilterPredicate<AlarmRuleFilterPredicate>; |
||||
@ -0,0 +1,123 @@ |
|||||
|
#### Active all time schedule format |
||||
|
|
||||
|
An attribute with a dynamic value for an active all-time schedule format must contain an empty JSON object or JSON in the following format: |
||||
|
|
||||
|
```javascript |
||||
|
{ |
||||
|
"type": "ANY_TIME" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### Specific time schedule format |
||||
|
|
||||
|
An attribute with a dynamic value for a specific schedule format must have JSON in the following format: |
||||
|
|
||||
|
```javascript |
||||
|
{ |
||||
|
"type": "SPECIFIC_TIME", |
||||
|
"daysOfWeek": [ |
||||
|
2, |
||||
|
4 |
||||
|
], |
||||
|
"endsOn": 0, |
||||
|
"startsOn": 0, |
||||
|
"timezone": "Europe/Kiev" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
<ul> |
||||
|
<li> |
||||
|
<b>timezone:</b> this value is used to designate the timezone you are using. |
||||
|
</li> |
||||
|
<li> |
||||
|
<b>daysOfWeek:</b> this value is used to designate the days in numerical representation (Monday - 1, Tuesday 2, etc.) on which the schedule will be active. |
||||
|
</li> |
||||
|
<li> |
||||
|
<b>startsOn:</b> this value is used to designate the timestamp in milliseconds, from which the schedule will be active for the designated days. |
||||
|
</li> |
||||
|
<li> |
||||
|
<b>endsOn:</b> this value is used to designate the timestamp in milliseconds until which the schedule will be active for the specified days. |
||||
|
</li> |
||||
|
</ul> |
||||
|
When <b>startsOn</b> and <b>endsOn</b> equals 0 it's means that the schedule will be active the whole day. |
||||
|
|
||||
|
#### Custom time schedule format |
||||
|
|
||||
|
An attribute with a dynamic value for a custom schedule format must have JSON in the following format: |
||||
|
|
||||
|
```javascript |
||||
|
{ |
||||
|
"type": "CUSTOM" |
||||
|
"timezone": "Europe/Kiev", |
||||
|
"items": [ |
||||
|
{ |
||||
|
"dayOfWeek": 1, |
||||
|
"enabled": true, |
||||
|
"endsOn": 0, |
||||
|
"startsOn": 0 |
||||
|
}, |
||||
|
{ |
||||
|
"dayOfWeek": 2, |
||||
|
"enabled": true, |
||||
|
"endsOn": 0, |
||||
|
"startsOn": 0 |
||||
|
}, |
||||
|
{ |
||||
|
"dayOfWeek": 3, |
||||
|
"enabled": true, |
||||
|
"endsOn": 0, |
||||
|
"startsOn": 0 |
||||
|
}, |
||||
|
{ |
||||
|
"dayOfWeek": 4, |
||||
|
"enabled": true, |
||||
|
"endsOn": 0, |
||||
|
"startsOn": 0 |
||||
|
}, |
||||
|
{ |
||||
|
"dayOfWeek": 5, |
||||
|
"enabled": true, |
||||
|
"endsOn": 0, |
||||
|
"startsOn": 0 |
||||
|
}, |
||||
|
{ |
||||
|
"dayOfWeek": 6, |
||||
|
"enabled": true, |
||||
|
"endsOn": 0, |
||||
|
"startsOn": 0 |
||||
|
}, |
||||
|
{ |
||||
|
"dayOfWeek": 7, |
||||
|
"enabled": true, |
||||
|
"endsOn": 0, |
||||
|
"startsOn": 0 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
<ul> |
||||
|
<li> |
||||
|
<b>timezone:</b> this value is used to designate the timezone you are using. |
||||
|
</li> |
||||
|
<li> |
||||
|
<b>items:</b> the array of values representing the days on which the schedule will be active. |
||||
|
</li> |
||||
|
</ul> |
||||
|
|
||||
|
One array item contains such fields: |
||||
|
<ul> |
||||
|
<li> |
||||
|
<b>dayOfWeek:</b> this value is used to designate the specified day in numerical representation (Monday - 1, Tuesday 2, etc.) on which the schedule will be active. |
||||
|
</li> |
||||
|
<li> |
||||
|
<b>enabled:</b> this <code>boolean</code> value, used to designate that the specified day in the schedule will be enabled. |
||||
|
</li> |
||||
|
<li> |
||||
|
<b>startsOn:</b> this value is used to designate the timestamp in milliseconds, from which the schedule will be active for the designated day. |
||||
|
</li> |
||||
|
<li> |
||||
|
<b>endsOn:</b> this value is used to designate the timestamp in milliseconds until which the schedule will be active for the specified day. |
||||
|
</li> |
||||
|
</ul> |
||||
|
When <b>startsOn</b> and <b>endsOn</b> equals 0 it's means that the schedule will be active the whole day. |
||||
Loading…
Reference in new issue