39 changed files with 1860 additions and 606 deletions
@ -0,0 +1,68 @@ |
|||
///
|
|||
/// 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 { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component'; |
|||
import { |
|||
CalculatedFieldDialogComponent |
|||
} from '@home/components/calculated-fields/components/dialog/calculated-field-dialog.component'; |
|||
import { |
|||
CalculatedFieldDebugDialogComponent |
|||
} from '@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component'; |
|||
import { |
|||
CalculatedFieldScriptTestDialogComponent |
|||
} from '@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component'; |
|||
import { |
|||
CalculatedFieldTestArgumentsComponent |
|||
} from '@home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component'; |
|||
import { |
|||
EntityDebugSettingsButtonComponent |
|||
} from '@home/components/entity/debug/entity-debug-settings-button.component'; |
|||
import { HomeComponentsModule } from '@home/components/home-components.module'; |
|||
import { |
|||
GeofencingConfigurationModule |
|||
} from '@home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.module'; |
|||
import { |
|||
SimpleConfigurationModule |
|||
} from '@home/components/calculated-fields/components/simple-configuration/simple-configuration.module'; |
|||
import { |
|||
PropagationConfigurationModule |
|||
} from '@home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module'; |
|||
|
|||
@NgModule({ |
|||
declarations: [ |
|||
CalculatedFieldsTableComponent, |
|||
CalculatedFieldDialogComponent, |
|||
CalculatedFieldDebugDialogComponent, |
|||
CalculatedFieldScriptTestDialogComponent, |
|||
CalculatedFieldTestArgumentsComponent, |
|||
], |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
GeofencingConfigurationModule, |
|||
EntityDebugSettingsButtonComponent, |
|||
HomeComponentsModule, |
|||
SimpleConfigurationModule, |
|||
PropagationConfigurationModule, |
|||
], |
|||
exports: [ |
|||
CalculatedFieldsTableComponent, |
|||
] |
|||
}) |
|||
export class CalculatedFieldsModule {} |
|||
@ -0,0 +1,45 @@ |
|||
///
|
|||
/// 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 { |
|||
CalculatedFieldArgumentPanelComponent |
|||
} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component'; |
|||
import { |
|||
CalculatedFieldArgumentsTableComponent |
|||
} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component'; |
|||
import { |
|||
PropagateArgumentsTableComponent |
|||
} from '@home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
], |
|||
declarations: [ |
|||
CalculatedFieldArgumentPanelComponent, |
|||
CalculatedFieldArgumentsTableComponent, |
|||
PropagateArgumentsTableComponent |
|||
], |
|||
exports: [ |
|||
CalculatedFieldArgumentsTableComponent, |
|||
PropagateArgumentsTableComponent |
|||
] |
|||
}) |
|||
export class CalculatedFieldArgumentsTableModule {} |
|||
@ -0,0 +1,116 @@ |
|||
///
|
|||
/// 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, |
|||
DestroyRef, |
|||
forwardRef, |
|||
OnInit, |
|||
Renderer2, |
|||
ViewContainerRef, |
|||
} from '@angular/core'; |
|||
import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, } from '@angular/forms'; |
|||
import { TbPopoverService } from '@shared/components/popover.service'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { |
|||
CalculatedFieldArgumentsTableComponent |
|||
} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component'; |
|||
import { ArgumentEntityType, ArgumentType, CalculatedFieldArgumentValue } from '@shared/models/calculated-field.models'; |
|||
import { isDefined } from '@core/utils'; |
|||
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-propagate-arguments-table', |
|||
templateUrl: './calculated-field-arguments-table.component.html', |
|||
styleUrls: [`calculated-field-arguments-table.component.scss`], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => PropagateArgumentsTableComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => PropagateArgumentsTableComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
}) |
|||
export class PropagateArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent implements OnInit { |
|||
|
|||
constructor( |
|||
protected fb: FormBuilder, |
|||
protected popoverService: TbPopoverService, |
|||
protected viewContainerRef: ViewContainerRef, |
|||
protected cd: ChangeDetectorRef, |
|||
protected renderer: Renderer2, |
|||
protected entityService: EntityService, |
|||
protected destroyRef: DestroyRef, |
|||
protected store: Store<AppState> |
|||
) { |
|||
super(fb, popoverService, viewContainerRef, cd, renderer, entityService, destroyRef, store) |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.updatedValue(); |
|||
} |
|||
|
|||
protected changeIsScriptMode(): void { |
|||
this.updatedValue(); |
|||
super.changeIsScriptMode(); |
|||
} |
|||
|
|||
private updatedValue() { |
|||
if (this.isScript) { |
|||
this.argumentNameColumn = 'common.name'; |
|||
this.argumentNameColumnCopy = 'calculated-fields.copy-argument-name'; |
|||
this.displayColumns = ['name', 'entityType', 'target', 'type', 'key', 'actions']; |
|||
this.panelAdditionalCtx = null; |
|||
} else { |
|||
this.argumentNameColumn = 'calculated-fields.output-key'; |
|||
this.argumentNameColumnCopy = 'calculated-fields.copy-output-key'; |
|||
this.displayColumns = ['name', 'type', 'key', 'actions']; |
|||
this.panelAdditionalCtx = { |
|||
argumentEntityTypes: [ArgumentEntityType.Current], |
|||
isOutputKey: true |
|||
}; |
|||
} |
|||
} |
|||
|
|||
protected isEditButtonShowBadge(argument: CalculatedFieldArgumentValue): boolean { |
|||
if (!this.isScript && isDefined(argument?.refEntityId)) { |
|||
return false; |
|||
} |
|||
return super.isEditButtonShowBadge(argument); |
|||
} |
|||
|
|||
protected updateErrorText(): void { |
|||
if (!this.isScript && this.argumentsFormArray.controls.some(control => isDefined(control.value?.refEntityId))) { |
|||
this.errorText = 'calculated-fields.hint.arguments-propagate-argument-entity-type'; |
|||
} else if (!this.isScript && this.argumentsFormArray.controls.some(control => control.value.refEntityKey.type === ArgumentType.Rolling)) { |
|||
this.errorText = 'calculated-fields.hint.arguments-propagate-arguments-with-rolling'; |
|||
} else if (this.argumentsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) { |
|||
this.errorText = 'calculated-fields.hint.arguments-entity-not-found'; |
|||
} else if (!this.argumentsFormArray.controls.length) { |
|||
this.errorText = 'calculated-fields.hint.arguments-empty'; |
|||
} else { |
|||
this.errorText = ''; |
|||
} |
|||
} |
|||
} |
|||
0
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.html → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html
0
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.html → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html
0
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.scss → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.scss
0
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.scss → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.scss
8
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts
8
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts
@ -0,0 +1,68 @@ |
|||
<!-- |
|||
|
|||
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]="geofencingConfiguration" class="tb-form-panel no-border no-padding"> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title tb-required" tb-hint-tooltip-icon="{{ 'calculated-fields.hint.entity-coordinates' | translate }}"> |
|||
{{ 'calculated-fields.entity-coordinates' | translate }} |
|||
</div> |
|||
<div class="flex items-start gap-3" formGroupName="entityCoordinates"> |
|||
<tb-entity-key-autocomplete class="flex-1" |
|||
placeholder="{{ 'calculated-fields.latitude-time-series-key' | translate }}" |
|||
requiredText="{{ 'calculated-fields.latitude-time-series-key-required' | translate }}" |
|||
formControlName="latitudeKeyName" |
|||
[dataKeyType]="DataKeyType.timeseries" |
|||
[entityFilter]="currentEntityFilter"/> |
|||
<tb-entity-key-autocomplete class="flex-1" |
|||
placeholder="{{ 'calculated-fields.longitude-time-series-key' | translate }}" |
|||
requiredText="{{ 'calculated-fields.longitude-time-series-key-required' | translate }}" |
|||
formControlName="longitudeKeyName" |
|||
[dataKeyType]="DataKeyType.timeseries" |
|||
[entityFilter]="currentEntityFilter"/> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title tb-required" tb-hint-tooltip-icon="{{ 'calculated-fields.hint.geofencing-zone-groups' | translate }}"> |
|||
{{ 'calculated-fields.geofencing-zone-groups' | translate }} |
|||
</div> |
|||
<tb-calculated-field-geofencing-zone-groups-table formControlName="zoneGroups" |
|||
[entityId]="entityId" |
|||
[tenantId]="tenantId" |
|||
[entityName]="entityName"/> |
|||
<div class="tb-form-row space-between flex-1 columns-xs" [class.!hidden]="!isRelatedEntity"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="scheduledUpdateEnabled"> |
|||
<div tb-hint-tooltip-icon="{{'calculated-fields.hint.zone-group-refresh-interval' | translate}}"> |
|||
{{ 'calculated-fields.zone-group-refresh-interval' | translate }} |
|||
</div> |
|||
</mat-slide-toggle> |
|||
<div class="flex flex-row items-center justify-start gap-2"> |
|||
<tb-time-unit-input required |
|||
inlineField |
|||
requiredText="{{ 'calculated-fields.hint.zone-group-refresh-interval-required' | translate }}" |
|||
minErrorText="{{ 'calculated-fields.hint.zone-group-refresh-interval-min' | translate: {min: minAllowedScheduledUpdateIntervalInSecForCF} }}" |
|||
[minTime]="minAllowedScheduledUpdateIntervalInSecForCF" |
|||
formControlName="scheduledUpdateInterval"> |
|||
</tb-time-unit-input> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<tb-calculate-field-output |
|||
formControlName="output" |
|||
[entityId]="entityId"> |
|||
</tb-calculate-field-output> |
|||
</div> |
|||
@ -0,0 +1,157 @@ |
|||
///
|
|||
/// 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, OnInit } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
ValidationErrors, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { |
|||
ArgumentEntityType, |
|||
CalculatedFieldGeofencing, |
|||
CalculatedFieldGeofencingConfiguration, |
|||
CalculatedFieldOutput, |
|||
CalculatedFieldType, |
|||
getCalculatedFieldCurrentEntityFilter, |
|||
OutputType |
|||
} from '@shared/models/calculated-field.models'; |
|||
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { getCurrentAuthState } from '@core/auth/auth.selectors'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { EntityFilter } from '@shared/models/query/query.models'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-geofencing-configuration', |
|||
templateUrl: './geofencing-configuration.component.html', |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => GeofencingConfigurationComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => GeofencingConfigurationComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
}) |
|||
export class GeofencingConfigurationComponent implements ControlValueAccessor, Validator, OnInit { |
|||
|
|||
@Input({required: true}) |
|||
entityId: EntityId; |
|||
|
|||
@Input({required: true}) |
|||
tenantId: string; |
|||
|
|||
@Input({required: true}) |
|||
entityName: string; |
|||
|
|||
readonly minAllowedScheduledUpdateIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedScheduledUpdateIntervalInSecForCF; |
|||
readonly DataKeyType = DataKeyType; |
|||
|
|||
geofencingConfiguration = this.fb.group({ |
|||
entityCoordinates: this.fb.group({ |
|||
latitudeKeyName: [null, [Validators.required]], |
|||
longitudeKeyName: [null, [Validators.required]], |
|||
}), |
|||
zoneGroups: this.fb.control<Record<string, CalculatedFieldGeofencing>>({}), |
|||
scheduledUpdateEnabled: [true], |
|||
scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF], |
|||
output: this.fb.control<CalculatedFieldOutput>({scope: AttributeScope.SERVER_SCOPE, type: OutputType.Timeseries}) |
|||
}); |
|||
|
|||
currentEntityFilter: EntityFilter; |
|||
isRelatedEntity: boolean; |
|||
|
|||
private propagateChange: (config: CalculatedFieldGeofencingConfiguration) => void = () => { }; |
|||
|
|||
constructor(private fb: FormBuilder, |
|||
private store: Store<AppState>) { |
|||
|
|||
this.geofencingConfiguration.get('zoneGroups').valueChanges |
|||
.pipe(takeUntilDestroyed()) |
|||
.subscribe((zoneGroups: Record<string, CalculatedFieldGeofencing>) => |
|||
this.checkRelatedEntity(zoneGroups) |
|||
); |
|||
|
|||
this.geofencingConfiguration.get('scheduledUpdateEnabled').valueChanges |
|||
.pipe(takeUntilDestroyed()) |
|||
.subscribe((value: boolean) => |
|||
this.checkScheduledUpdateEnabled(value) |
|||
); |
|||
|
|||
this.geofencingConfiguration.valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe(() => { |
|||
this.updatedModel(this.geofencingConfiguration.getRawValue() as any); |
|||
}) |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId); |
|||
} |
|||
|
|||
validate(): ValidationErrors | null { |
|||
return this.geofencingConfiguration.valid || this.geofencingConfiguration.status === "DISABLED" ? null : { geofencingConfigError: false }; |
|||
} |
|||
|
|||
writeValue(config: CalculatedFieldGeofencingConfiguration): void { |
|||
this.geofencingConfiguration.patchValue(config, {emitEvent: false}); |
|||
this.checkRelatedEntity(this.geofencingConfiguration.get('zoneGroups').value); |
|||
this.checkScheduledUpdateEnabled(this.geofencingConfiguration.get('scheduledUpdateEnabled').value); |
|||
} |
|||
|
|||
registerOnChange(fn: (config: CalculatedFieldGeofencingConfiguration) => void): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(_: any): void { } |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
if (isDisabled) { |
|||
this.geofencingConfiguration.disable({emitEvent: false}); |
|||
} else { |
|||
this.geofencingConfiguration.enable({emitEvent: false}); |
|||
this.checkScheduledUpdateEnabled(this.geofencingConfiguration.get('scheduledUpdateEnabled').value); |
|||
} |
|||
} |
|||
|
|||
private updatedModel(value: CalculatedFieldGeofencingConfiguration) { |
|||
value.type = CalculatedFieldType.GEOFENCING; |
|||
this.propagateChange(value) |
|||
} |
|||
|
|||
private checkScheduledUpdateEnabled(value: boolean) { |
|||
if (value) { |
|||
this.geofencingConfiguration.get('scheduledUpdateInterval').enable({emitEvent: false}); |
|||
} else { |
|||
this.geofencingConfiguration.get('scheduledUpdateInterval').disable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
private checkRelatedEntity(zoneGroups: Record<string, CalculatedFieldGeofencing>) { |
|||
this.isRelatedEntity = Object.values(zoneGroups).some(zone => zone.refDynamicSourceConfiguration?.type === ArgumentEntityType.RelationQuery); |
|||
} |
|||
} |
|||
@ -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.
|
|||
///
|
|||
|
|||
import { NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { |
|||
CalculatedFieldGeofencingZoneGroupsTableComponent |
|||
} from '@home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component'; |
|||
import { |
|||
CalculatedFieldGeofencingZoneGroupsPanelComponent |
|||
} from '@home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { |
|||
GeofencingConfigurationComponent |
|||
} from '@home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component'; |
|||
import { |
|||
CalculatedFieldOutputModule |
|||
} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
CalculatedFieldOutputModule |
|||
], |
|||
declarations: [ |
|||
CalculatedFieldGeofencingZoneGroupsTableComponent, |
|||
CalculatedFieldGeofencingZoneGroupsPanelComponent, |
|||
GeofencingConfigurationComponent |
|||
], |
|||
exports: [ |
|||
CalculatedFieldGeofencingZoneGroupsTableComponent, |
|||
CalculatedFieldGeofencingZoneGroupsPanelComponent, |
|||
GeofencingConfigurationComponent |
|||
] |
|||
}) |
|||
export class GeofencingConfigurationModule { |
|||
|
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
<!-- |
|||
|
|||
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" [formGroup]="outputForm"> |
|||
<div class="tb-form-panel-title">{{ 'calculated-fields.output' | translate }}</div> |
|||
<div class="flex items-center gap-3"> |
|||
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label>{{ 'calculated-fields.output-type' | translate }}</mat-label> |
|||
<mat-select formControlName="type"> |
|||
@for (type of outputTypes; track type) { |
|||
<mat-option [value]="type">{{ OutputTypeTranslations.get(type) | translate }}</mat-option> |
|||
} |
|||
</mat-select> |
|||
</mat-form-field> |
|||
@if (outputForm.get('type').value === OutputType.Attribute |
|||
&& (entityId.entityType === EntityType.DEVICE || entityId.entityType === EntityType.DEVICE_PROFILE)) { |
|||
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label>{{ 'calculated-fields.attribute-scope' | translate }}</mat-label> |
|||
<mat-select formControlName="scope" class="w-full"> |
|||
<mat-option [value]="AttributeScope.SERVER_SCOPE"> |
|||
{{ 'calculated-fields.server-attributes' | translate }} |
|||
</mat-option> |
|||
<mat-option [value]="AttributeScope.SHARED_SCOPE"> |
|||
{{ 'calculated-fields.shared-attributes' | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
} |
|||
</div> |
|||
@if (simpleMode) { |
|||
<div class="flex items-start gap-3"> |
|||
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label> |
|||
{{ |
|||
(outputForm.get('type').value === OutputType.Timeseries |
|||
? 'calculated-fields.timeseries-key' |
|||
: 'calculated-fields.attribute-key') |
|||
| translate |
|||
}} |
|||
</mat-label> |
|||
<input matInput formControlName="name" required> |
|||
@if (outputForm.get('name').errors && outputForm.get('name').touched) { |
|||
<mat-error> |
|||
@if (outputForm.get('name').hasError('required')) { |
|||
{{ 'common.hint.key-required' | translate }} |
|||
} @else if (outputForm.get('name').hasError('pattern')) { |
|||
{{ 'common.hint.key-pattern' | translate }} |
|||
} @else if (outputForm.get('name').hasError('maxlength')) { |
|||
{{ 'common.hint.key-max-length' | translate }} |
|||
} |
|||
</mat-error> |
|||
} |
|||
</mat-form-field> |
|||
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label>{{ 'calculated-fields.decimals-by-default' | translate }}</mat-label> |
|||
<input matInput type="number" formControlName="decimalsByDefault"> |
|||
@if (outputForm.get('decimalsByDefault').errors && outputForm.get('decimalsByDefault').touched) { |
|||
<mat-error>{{ 'calculated-fields.hint.decimals-range' | translate }}</mat-error> |
|||
} |
|||
</mat-form-field> |
|||
</div> |
|||
<ng-content select=".simpleMode"></ng-content> |
|||
<!-- <div class="tb-form-row" [formGroup]="configFormGroup"--> |
|||
<!-- *ngIf="outputFormGroup.get('type').value === OutputType.Timeseries">--> |
|||
<!-- <mat-slide-toggle class="mat-slide" formControlName="useLatestTs">--> |
|||
<!-- <div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.use-latest-timestamp' | translate }}" translate>--> |
|||
<!-- calculated-fields.use-latest-timestamp--> |
|||
<!-- </div>--> |
|||
<!-- </mat-slide-toggle>--> |
|||
<!-- </div>--> |
|||
} |
|||
</div> |
|||
@ -0,0 +1,148 @@ |
|||
///
|
|||
/// 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, inject, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
ValidationErrors, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
|||
import { |
|||
CalculatedFieldOutput, |
|||
CalculatedFieldSimpleOutput, |
|||
OutputType, |
|||
OutputTypeTranslations |
|||
} from '@shared/models/calculated-field.models'; |
|||
import { digitsRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-calculate-field-output', |
|||
templateUrl: './calculated-field-output.component.html', |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => CalculatedFieldOutputComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => CalculatedFieldOutputComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
}) |
|||
export class CalculatedFieldOutputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { |
|||
|
|||
@Input() |
|||
simpleMode = false; |
|||
|
|||
@Input({required: true}) |
|||
entityId: EntityId; |
|||
|
|||
readonly outputTypes = Object.values(OutputType) as OutputType[]; |
|||
readonly OutputType = OutputType; |
|||
readonly AttributeScope = AttributeScope; |
|||
readonly OutputTypeTranslations = OutputTypeTranslations; |
|||
readonly EntityType = EntityType; |
|||
|
|||
private fb = inject(FormBuilder); |
|||
private destroyRef = inject(DestroyRef); |
|||
|
|||
outputForm = this.fb.group({ |
|||
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], |
|||
scope: [{value: AttributeScope.SERVER_SCOPE, disabled: true}], |
|||
type: [OutputType.Timeseries], |
|||
decimalsByDefault: [null as number, [Validators.min(0), Validators.max(15), Validators.pattern(digitsRegex)]], |
|||
}); |
|||
|
|||
private propagateChange: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void = () => { }; |
|||
|
|||
ngOnInit() { |
|||
this.outputForm.get('type').valueChanges |
|||
.pipe(takeUntilDestroyed(this.destroyRef)) |
|||
.subscribe(type => this.toggleScopeByOutputType(type)); |
|||
|
|||
this.updatedFormWithMode(); |
|||
|
|||
this.outputForm.valueChanges.pipe( |
|||
takeUntilDestroyed(this.destroyRef) |
|||
).subscribe((value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => { |
|||
this.updatedModel(value) |
|||
}) |
|||
} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
for (const propName of Object.keys(changes)) { |
|||
const change = changes[propName]; |
|||
if (change.currentValue !== change.previousValue) { |
|||
if (propName === 'simpleMode') { |
|||
this.updatedFormWithMode(); |
|||
if (!change.firstChange) { |
|||
this.outputForm.updateValueAndValidity(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
validate(): ValidationErrors | null { |
|||
return this.outputForm.valid ? null : {outputConfig: false}; |
|||
} |
|||
|
|||
writeValue(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput): void { |
|||
this.outputForm.patchValue(value, {emitEvent: false}); |
|||
this.outputForm.get('type').updateValueAndValidity({onlySelf: true}); |
|||
} |
|||
|
|||
registerOnChange(fn: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(_: any): void { } |
|||
|
|||
private updatedModel(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) { |
|||
if (this.simpleMode && 'name' in value) { |
|||
value.name = value.name?.trim() ?? ''; |
|||
} |
|||
this.propagateChange(value); |
|||
} |
|||
|
|||
private toggleScopeByOutputType(type: OutputType): void { |
|||
if (type === OutputType.Attribute) { |
|||
this.outputForm.get('scope').enable({emitEvent: false}); |
|||
} else { |
|||
this.outputForm.get('scope').disable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
private updatedFormWithMode(): void { |
|||
if (this.simpleMode) { |
|||
this.outputForm.get('name').enable({emitEvent: false}); |
|||
this.outputForm.get('decimalsByDefault').enable({emitEvent: false}); |
|||
} else { |
|||
this.outputForm.get('name').disable({emitEvent: false}); |
|||
this.outputForm.get('decimalsByDefault').disable({emitEvent: false}); |
|||
} |
|||
} |
|||
} |
|||
@ -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.
|
|||
///
|
|||
|
|||
import { NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { |
|||
CalculatedFieldOutputComponent |
|||
} from '@home/components/calculated-fields/components/output/calculated-field-output.component'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
], |
|||
declarations: [ |
|||
CalculatedFieldOutputComponent |
|||
], |
|||
exports: [ |
|||
CalculatedFieldOutputComponent |
|||
] |
|||
}) |
|||
export class CalculatedFieldOutputModule { } |
|||
@ -0,0 +1,99 @@ |
|||
<!-- |
|||
|
|||
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]="propagateConfiguration" class="tb-form-panel no-border no-padding"> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title" tbTruncateWithTooltip tb-hint-tooltip-icon="{{ 'calculated-fields.hint.propagation-path-related-entities' | translate }}"> |
|||
{{ 'calculated-fields.propagation-path-related-entities' | translate }} |
|||
</div> |
|||
<div class="flex gap-3 xs:flex-col"> |
|||
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic" hideRequiredMarker> |
|||
<mat-label>{{ 'calculated-fields.direction' | translate }}</mat-label> |
|||
<mat-select formControlName="direction"> |
|||
@for (direction of Directions; track direction) { |
|||
<mat-option [value]="direction">{{ PropagationDirectionTranslations.get(direction) | translate }}</mat-option> |
|||
} |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)" |
|||
class="flex-1" |
|||
panelWidth="" |
|||
additionalClass="" |
|||
required |
|||
[label]="'calculated-fields.relation-type' | translate" |
|||
[errorText]="'calculated-fields.hint.relation-type-required' | translate" |
|||
formControlName="relationType"> |
|||
</tb-string-autocomplete> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-panel"> |
|||
<div class="flex flex-row items-center justify-between xs:flex-col xs:items-start xs:gap-3"> |
|||
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'calculated-fields.hint.data-propagate' | translate }}"> |
|||
{{ 'calculated-fields.data-propagate' | translate }} |
|||
</div> |
|||
<tb-toggle-select formControlName="applyExpressionToResolvedArguments"> |
|||
<tb-toggle-option [value]="false">{{ 'calculated-fields.propagate-type.arguments-only' | translate }}</tb-toggle-option> |
|||
<tb-toggle-option [value]="true">{{ 'calculated-fields.propagate-type.expression-result' | translate }}</tb-toggle-option> |
|||
</tb-toggle-select> |
|||
</div> |
|||
<tb-propagate-arguments-table formControlName="arguments" |
|||
[entityId]="entityId" |
|||
[tenantId]="tenantId" |
|||
[entityName]="entityName" |
|||
[isScript]="this.propagateConfiguration.get('applyExpressionToResolvedArguments').value"/> |
|||
</div> |
|||
<div class="tb-form-panel no-gap" [class.!hidden]="!this.propagateConfiguration.get('applyExpressionToResolvedArguments').value"> |
|||
<div class="tb-form-panel-title tb-required"> |
|||
{{ 'calculated-fields.expression' | translate }} |
|||
</div> |
|||
<div> |
|||
<tb-js-func required |
|||
formControlName="expression" |
|||
functionName="calculate" |
|||
[functionArgs]="functionArgs$ | async" |
|||
[disableUndefinedCheck]="true" |
|||
[scriptLanguage]="ScriptLanguage.TBEL" |
|||
[highlightRules]="argumentsHighlightRules$ | async" |
|||
[editorCompleter]="argumentsEditorCompleter$ | async" |
|||
[helpPopupStyle]="{ width: '1200px' }" |
|||
helpId="calculated-field/expression_fn"> |
|||
<div toolbarPrefixButton |
|||
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }} |
|||
</div> |
|||
<button toolbarSuffixButton |
|||
mat-icon-button |
|||
matTooltip="{{ 'calculated-fields.test-expression-function' | translate }}" |
|||
matTooltipPosition="above" |
|||
class="tb-mat-32" |
|||
[disabled]="propagateConfiguration.get('arguments').invalid" |
|||
(click)="onTestScript()"> |
|||
<mat-icon class="material-icons" color="primary">bug_report</mat-icon> |
|||
</button> |
|||
</tb-js-func> |
|||
<div> |
|||
<button mat-button mat-raised-button color="primary" |
|||
type="button" |
|||
(click)="onTestScript()" |
|||
[disabled]="propagateConfiguration.get('arguments').invalid"> |
|||
{{ 'calculated-fields.test-expression-function' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<tb-calculate-field-output formControlName="output" [entityId]="entityId"> |
|||
</tb-calculate-field-output> |
|||
</div> |
|||
@ -0,0 +1,174 @@ |
|||
///
|
|||
/// 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 } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
ValidationErrors, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { |
|||
calculatedFieldDefaultScript, |
|||
CalculatedFieldOutput, |
|||
CalculatedFieldPropagationConfiguration, |
|||
CalculatedFieldType, |
|||
getCalculatedFieldArgumentsEditorCompleter, |
|||
getCalculatedFieldArgumentsHighlights, |
|||
OutputType, |
|||
PropagationDirectionTranslations, |
|||
PropagationWithExpression |
|||
} from '@shared/models/calculated-field.models'; |
|||
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
|||
import { map } from 'rxjs/operators'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { ScriptLanguage } from '@app/shared/models/rule-node.models'; |
|||
import { EntitySearchDirection } from '@shared/models/relation.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-propagation-configuration', |
|||
templateUrl: './propagation-configuration.component.html', |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => PropagationConfigurationComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => PropagationConfigurationComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
}) |
|||
export class PropagationConfigurationComponent implements ControlValueAccessor, Validator { |
|||
|
|||
@Input({required: true}) |
|||
entityId: EntityId; |
|||
|
|||
@Input({required: true}) |
|||
tenantId: string; |
|||
|
|||
@Input({required: true}) |
|||
entityName: string; |
|||
|
|||
@Input({required: true}) |
|||
testScript: () => Observable<string>; |
|||
|
|||
propagateConfiguration = this.fb.group({ |
|||
arguments: this.fb.control({}), |
|||
applyExpressionToResolvedArguments: [false], |
|||
direction: [EntitySearchDirection.TO, Validators.required], |
|||
relationType: ['Contains', Validators.required], |
|||
expression: [calculatedFieldDefaultScript], |
|||
output: this.fb.control<CalculatedFieldOutput>({ |
|||
scope: AttributeScope.SERVER_SCOPE, |
|||
type: OutputType.Timeseries, |
|||
}), |
|||
}); |
|||
|
|||
readonly ScriptLanguage = ScriptLanguage; |
|||
readonly CalculatedFieldType = CalculatedFieldType; |
|||
readonly OutputType = OutputType; |
|||
readonly Directions = Object.values(EntitySearchDirection) as Array<EntitySearchDirection>; |
|||
readonly PropagationDirectionTranslations = PropagationDirectionTranslations; |
|||
|
|||
functionArgs$ = this.propagateConfiguration.get('arguments').valueChanges.pipe( |
|||
map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)]) |
|||
); |
|||
|
|||
argumentsEditorCompleter$ = this.propagateConfiguration.get('arguments').valueChanges.pipe( |
|||
map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {})) |
|||
); |
|||
|
|||
argumentsHighlightRules$ = this.propagateConfiguration.get('arguments').valueChanges.pipe( |
|||
map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) |
|||
); |
|||
|
|||
private propagateChange: (config: CalculatedFieldPropagationConfiguration) => void = () => { }; |
|||
|
|||
constructor(private fb: FormBuilder) { |
|||
this.propagateConfiguration.get('applyExpressionToResolvedArguments').valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe(() => { |
|||
this.updatedFormWithScript(); |
|||
}) |
|||
|
|||
this.propagateConfiguration.valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe((value: CalculatedFieldPropagationConfiguration) => { |
|||
this.updatedModel(value); |
|||
}) |
|||
} |
|||
|
|||
validate(): ValidationErrors | null { |
|||
return this.propagateConfiguration.valid || this.propagateConfiguration.status === "DISABLED" ? null : {invalidPropagateConfig: false}; |
|||
} |
|||
|
|||
writeValue(value: PropagationWithExpression): void { |
|||
value.expression = value.expression ?? calculatedFieldDefaultScript; |
|||
this.propagateConfiguration.patchValue(value, {emitEvent: false}); |
|||
this.updatedFormWithScript(); |
|||
setTimeout(() => { |
|||
this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: (config: CalculatedFieldPropagationConfiguration) => void): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(_: any): void { } |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
if (isDisabled) { |
|||
this.propagateConfiguration.disable({emitEvent: false}); |
|||
} else { |
|||
this.propagateConfiguration.enable({emitEvent: false}); |
|||
this.updatedFormWithScript(); |
|||
} |
|||
} |
|||
|
|||
onTestScript() { |
|||
this.testScript().subscribe((expression) => { |
|||
this.propagateConfiguration.get('expression').setValue(expression); |
|||
this.propagateConfiguration.get('expression').markAsDirty(); |
|||
}) |
|||
} |
|||
|
|||
fetchOptions(searchText: string): Observable<Array<string>> { |
|||
const search = searchText ? searchText?.toLowerCase() : ''; |
|||
return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search)))); |
|||
} |
|||
|
|||
private updatedModel(value: CalculatedFieldPropagationConfiguration): void { |
|||
value.type = CalculatedFieldType.PROPAGATION; |
|||
this.propagateChange(value); |
|||
} |
|||
|
|||
private updatedFormWithScript() { |
|||
if (this.propagateConfiguration.get('applyExpressionToResolvedArguments').value) { |
|||
this.propagateConfiguration.get('expression').enable({emitEvent: false}); |
|||
} else { |
|||
this.propagateConfiguration.get('expression').disable({emitEvent: false}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
///
|
|||
/// 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 { |
|||
CalculatedFieldOutputModule |
|||
} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; |
|||
import { |
|||
CalculatedFieldArgumentsTableModule |
|||
} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; |
|||
import { |
|||
PropagationConfigurationComponent |
|||
} from '@home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
CalculatedFieldOutputModule, |
|||
CalculatedFieldArgumentsTableModule, |
|||
], |
|||
declarations: [ |
|||
PropagationConfigurationComponent, |
|||
], |
|||
exports: [ |
|||
PropagationConfigurationComponent, |
|||
] |
|||
}) |
|||
export class PropagationConfigurationModule { } |
|||
@ -0,0 +1,98 @@ |
|||
<!-- |
|||
|
|||
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]="simpleConfiguration" class="tb-form-panel no-border no-padding"> |
|||
<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]="entityId" |
|||
[tenantId]="tenantId" |
|||
[entityName]="entityName" |
|||
[isScript]="isScript" /> |
|||
</div> |
|||
<div class="tb-form-panel no-gap"> |
|||
<div class="tb-form-panel-title tb-required"> |
|||
{{ (isScript ? 'calculated-fields.type.script' : 'calculated-fields.expression') | translate }} |
|||
</div> |
|||
<mat-form-field class="mt-3" appearance="outline" subscriptSizing="dynamic" [class.hidden]="isScript"> |
|||
<input matInput formControlName="expressionSIMPLE" maxlength="255" [placeholder]="'(temperature - 32) / 1.8'" |
|||
required> |
|||
<div matSuffix |
|||
class="pr-2" |
|||
[tb-help-popup]="'math/math-methods_fn'" |
|||
tb-help-popup-placement="left" |
|||
[tb-help-popup-style]="{maxWidth: '970px'}"> |
|||
</div> |
|||
@if (simpleConfiguration.get('expressionSIMPLE').errors && simpleConfiguration.get('expressionSIMPLE').touched) { |
|||
<mat-error> |
|||
@if (simpleConfiguration.get('expressionSIMPLE').hasError('required')) { |
|||
{{ 'calculated-fields.hint.expression-required' | translate }} |
|||
} @else if (simpleConfiguration.get('expressionSIMPLE').hasError('pattern')) { |
|||
{{ 'calculated-fields.hint.expression-invalid' | translate }} |
|||
} @else if (simpleConfiguration.get('expressionSIMPLE').hasError('maxLength')) { |
|||
{{ 'calculated-fields.hint.expression-max-length' | translate }} |
|||
} |
|||
</mat-error> |
|||
} @else { |
|||
<mat-hint>{{ 'calculated-fields.hint.expression' | translate }}</mat-hint> |
|||
} |
|||
</mat-form-field> |
|||
<div [class.hidden]="!isScript"> |
|||
<tb-js-func required |
|||
formControlName="expressionSCRIPT" |
|||
functionName="calculate" |
|||
[functionArgs]="functionArgs$ | async" |
|||
[disableUndefinedCheck]="true" |
|||
[scriptLanguage]="ScriptLanguage.TBEL" |
|||
[highlightRules]="argumentsHighlightRules$ | async" |
|||
[editorCompleter]="argumentsEditorCompleter$ | async" |
|||
[helpPopupStyle]="{ width: '1200px' }" |
|||
helpId="calculated-field/expression_fn"> |
|||
<div toolbarPrefixButton |
|||
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }} |
|||
</div> |
|||
<button toolbarSuffixButton |
|||
mat-icon-button |
|||
matTooltip="{{ 'calculated-fields.test-script-function' | translate }}" |
|||
matTooltipPosition="above" |
|||
class="tb-mat-32" |
|||
[disabled]="simpleConfiguration.get('arguments').invalid" |
|||
(click)="onTestScript()"> |
|||
<mat-icon class="material-icons" color="primary">bug_report</mat-icon> |
|||
</button> |
|||
</tb-js-func> |
|||
<div> |
|||
<button mat-button mat-raised-button color="primary" |
|||
type="button" |
|||
(click)="onTestScript()" |
|||
[disabled]="simpleConfiguration.get('arguments').invalid"> |
|||
{{ 'calculated-fields.test-script-function' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<tb-calculate-field-output formControlName="output" [simpleMode]="!isScript" [entityId]="entityId"> |
|||
<div class="tb-form-row simpleMode" |
|||
[class.!hidden]="simpleConfiguration.get('output').value.type !== OutputType.Timeseries"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="useLatestTs"> |
|||
<div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.use-latest-timestamp' | translate }}" translate> |
|||
calculated-fields.use-latest-timestamp |
|||
</div> |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</tb-calculate-field-output> |
|||
</div> |
|||
@ -0,0 +1,206 @@ |
|||
///
|
|||
/// 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, OnChanges, SimpleChanges } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
ValidationErrors, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; |
|||
import { |
|||
calculatedFieldDefaultScript, |
|||
CalculatedFieldScriptConfiguration, |
|||
CalculatedFieldSimpleConfiguration, |
|||
CalculatedFieldSimpleOutput, |
|||
CalculatedFieldType, |
|||
getCalculatedFieldArgumentsEditorCompleter, |
|||
getCalculatedFieldArgumentsHighlights, |
|||
OutputType |
|||
} from '@shared/models/calculated-field.models'; |
|||
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { deepClone } from '@core/utils'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { Observable } from 'rxjs'; |
|||
import { ScriptLanguage } from '@shared/models/rule-node.models'; |
|||
import { map } from 'rxjs/operators'; |
|||
|
|||
type SimpeConfiguration = CalculatedFieldSimpleConfiguration | CalculatedFieldScriptConfiguration; |
|||
|
|||
@Component({ |
|||
selector: 'tb-simple-configuration', |
|||
templateUrl: './simple-configuration.component.html', |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => SimpleConfigurationComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => SimpleConfigurationComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
}) |
|||
export class SimpleConfigurationComponent implements ControlValueAccessor, Validator, OnChanges { |
|||
|
|||
@Input() |
|||
isScript: boolean; |
|||
|
|||
@Input({required: true}) |
|||
entityId: EntityId; |
|||
|
|||
@Input({required: true}) |
|||
tenantId: string; |
|||
|
|||
@Input({required: true}) |
|||
entityName: string; |
|||
|
|||
@Input({required: true}) |
|||
testScript: () => Observable<string>; |
|||
|
|||
simpleConfiguration = this.fb.group({ |
|||
arguments: this.fb.control({}), |
|||
expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], |
|||
expressionSCRIPT: [calculatedFieldDefaultScript], |
|||
output: this.fb.control<CalculatedFieldSimpleOutput>({ |
|||
name: '', |
|||
scope: AttributeScope.SERVER_SCOPE, |
|||
type: OutputType.Timeseries, |
|||
decimalsByDefault: null |
|||
}), |
|||
useLatestTs: [false] |
|||
}); |
|||
|
|||
readonly ScriptLanguage = ScriptLanguage; |
|||
readonly OutputType = OutputType; |
|||
|
|||
functionArgs$ = this.simpleConfiguration.get('arguments').valueChanges.pipe( |
|||
map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)]) |
|||
); |
|||
|
|||
argumentsEditorCompleter$ = this.simpleConfiguration.get('arguments').valueChanges.pipe( |
|||
map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {})) |
|||
); |
|||
|
|||
argumentsHighlightRules$ = this.simpleConfiguration.get('arguments').valueChanges.pipe( |
|||
map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) |
|||
); |
|||
|
|||
private propagateChange: (config: SimpeConfiguration) => void = () => { }; |
|||
|
|||
constructor(private fb: FormBuilder) { |
|||
this.simpleConfiguration.get('output').valueChanges.pipe( |
|||
takeUntilDestroyed(), |
|||
).subscribe(() => { |
|||
this.toggleScopeByOutputType(); |
|||
}); |
|||
|
|||
this.simpleConfiguration.valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe((value) => { |
|||
const { expressionSIMPLE, expressionSCRIPT, ...config } = value; |
|||
const cfConfig = config as SimpeConfiguration; |
|||
cfConfig.expression = this.isScript ? expressionSCRIPT : expressionSIMPLE; |
|||
this.updatedModel(cfConfig); |
|||
}) |
|||
} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
for (const propName of Object.keys(changes)) { |
|||
const change = changes[propName]; |
|||
if (change.currentValue !== change.previousValue) { |
|||
if (propName === 'isScript') { |
|||
this.updatedFormWithScript(); |
|||
if (!change.firstChange) { |
|||
this.simpleConfiguration.updateValueAndValidity(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
validate(): ValidationErrors | null { |
|||
return this.simpleConfiguration.valid || this.simpleConfiguration.status === "DISABLED" ? null : {invalidSimpleConfig: false}; |
|||
} |
|||
|
|||
writeValue(value: SimpeConfiguration): void { |
|||
const formValue: any = deepClone(value); |
|||
if (this.isScript) { |
|||
formValue.expressionSCRIPT = formValue.expression ?? calculatedFieldDefaultScript; |
|||
} else { |
|||
formValue.expressionSIMPLE = formValue.expression; |
|||
} |
|||
this.simpleConfiguration.patchValue(formValue, {emitEvent: false}); |
|||
this.updatedFormWithScript(); |
|||
setTimeout(() => { |
|||
this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: (config: SimpeConfiguration) => void): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(_: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
if (isDisabled) { |
|||
this.simpleConfiguration.disable({emitEvent: false}); |
|||
} else { |
|||
this.simpleConfiguration.enable({emitEvent: false}); |
|||
this.updatedFormWithScript(); |
|||
} |
|||
} |
|||
|
|||
onTestScript() { |
|||
this.testScript().subscribe((expression) => { |
|||
this.simpleConfiguration.get('expressionSCRIPT').setValue(expression); |
|||
this.simpleConfiguration.get('expressionSCRIPT').markAsDirty(); |
|||
}) |
|||
} |
|||
|
|||
private updatedModel(value: SimpeConfiguration): void { |
|||
value.type = this.isScript ? CalculatedFieldType.SCRIPT : CalculatedFieldType.SIMPLE; |
|||
this.propagateChange(value); |
|||
} |
|||
|
|||
private updatedFormWithScript() { |
|||
if (this.isScript) { |
|||
this.simpleConfiguration.get('expressionSIMPLE').disable({emitEvent: false}); |
|||
this.simpleConfiguration.get('expressionSCRIPT').enable({emitEvent: false}); |
|||
} else { |
|||
this.simpleConfiguration.get('expressionSIMPLE').enable({emitEvent: false}); |
|||
this.simpleConfiguration.get('expressionSCRIPT').disable({emitEvent: false}); |
|||
} |
|||
this.toggleScopeByOutputType(); |
|||
} |
|||
|
|||
private toggleScopeByOutputType(): void { |
|||
if (this.isScript || this.simpleConfiguration.get('output').value.type === OutputType.Attribute) { |
|||
this.simpleConfiguration.get('useLatestTs').disable({emitEvent: false}); |
|||
} else { |
|||
this.simpleConfiguration.get('useLatestTs').enable({emitEvent: false}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
///
|
|||
/// 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 { |
|||
SimpleConfigurationComponent |
|||
} from '@home/components/calculated-fields/components/simple-configuration/simple-configuration.component'; |
|||
import { |
|||
CalculatedFieldOutputModule |
|||
} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; |
|||
import { |
|||
CalculatedFieldArgumentsTableModule |
|||
} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
CalculatedFieldOutputModule, |
|||
CalculatedFieldArgumentsTableModule, |
|||
], |
|||
declarations: [ |
|||
SimpleConfigurationComponent, |
|||
], |
|||
exports: [ |
|||
SimpleConfigurationComponent |
|||
] |
|||
}) |
|||
export class SimpleConfigurationModule {} |
|||
Loading…
Reference in new issue