diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java index e390d9e55c..c55ec00379 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java @@ -52,9 +52,11 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF protected void processRestoredState(CalculatedFieldStateProto stateMsg) { var id = fromProto(stateMsg.getId()); var state = fromProto(stateMsg); + processRestoredState(id, state); + } + + protected void processRestoredState(CalculatedFieldEntityCtxId id, CalculatedFieldState state) { actorSystemContext.tell(new CalculatedFieldStateRestoreMsg(id, state)); } - - } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java index cce7e5ef22..959522ca63 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java @@ -24,10 +24,15 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.queue.TbQueueCallback; +import org.thingsboard.server.queue.TbQueueMsgHeaders; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; @@ -46,6 +51,11 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; +import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToString; +import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToUuid; +import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.stringToBytes; +import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.uuidToBytes; + @Service @RequiredArgsConstructor @Slf4j @@ -81,7 +91,11 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta .msgPackProcessor((msgs, consumer, config) -> { for (TbProtoQueueMsg msg : msgs) { try { - processRestoredState(msg.getValue()); + if (msg.getValue() != null) { + processRestoredState(msg.getValue()); + } else { + processRestoredState(getStateId(msg.getHeaders()), null); + } } catch (Throwable t) { log.error("Failed to process state message: {}", msg, t); } @@ -103,7 +117,11 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta @Override protected void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback) { TopicPartitionInfo tpi = partitionService.resolve(QueueKey.CF_STATES, stateId.entityId()); - stateProducer.send(tpi, stateId.toKey(), new TbProtoQueueMsg<>(stateId.entityId().getId(), stateMsgProto), new TbQueueCallback() { + TbProtoQueueMsg msg = new TbProtoQueueMsg<>(stateId.entityId().getId(), stateMsgProto); + if (stateMsgProto == null) { + putStateId(msg.getHeaders(), stateId); + } + stateProducer.send(tpi, stateId.toKey(), msg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { if (callback != null) { @@ -122,7 +140,7 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta @Override protected void doRemove(CalculatedFieldEntityCtxId stateId, TbCallback callback) { - //TODO: vklimov + doPersist(stateId, null, callback); } @Override @@ -138,6 +156,20 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta log.info("Restored {} calculated field states in {} ms", counter.get(), System.currentTimeMillis() - startTs); } + private void putStateId(TbQueueMsgHeaders headers, CalculatedFieldEntityCtxId stateId) { + headers.put("tenantId", uuidToBytes(stateId.tenantId().getId())); + headers.put("cfId", uuidToBytes(stateId.cfId().getId())); + headers.put("entityId", uuidToBytes(stateId.entityId().getId())); + headers.put("entityType", stringToBytes(stateId.entityId().getEntityType().name())); + } + + private CalculatedFieldEntityCtxId getStateId(TbQueueMsgHeaders headers) { + TenantId tenantId = TenantId.fromUUID(bytesToUuid(headers.get("tenantId"))); + CalculatedFieldId cfId = new CalculatedFieldId(bytesToUuid(headers.get("cfId"))); + EntityId entityId = EntityIdFactory.getByTypeAndUuid(bytesToString(headers.get("entityType")), bytesToUuid(headers.get("entityId"))); + return new CalculatedFieldEntityCtxId(tenantId, cfId, entityId); + } + @PreDestroy private void preDestroy() { stateConsumer.stop(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueTemplate.java index f11eaaef48..51ed1bb05f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueTemplate.java @@ -24,35 +24,36 @@ public class AbstractTbQueueTemplate { protected static final String RESPONSE_TOPIC_HEADER = "responseTopic"; protected static final String EXPIRE_TS_HEADER = "expireTs"; - protected byte[] uuidToBytes(UUID uuid) { + public static byte[] uuidToBytes(UUID uuid) { ByteBuffer buf = ByteBuffer.allocate(16); buf.putLong(uuid.getMostSignificantBits()); buf.putLong(uuid.getLeastSignificantBits()); return buf.array(); } - protected static UUID bytesToUuid(byte[] bytes) { + public static UUID bytesToUuid(byte[] bytes) { ByteBuffer bb = ByteBuffer.wrap(bytes); long firstLong = bb.getLong(); long secondLong = bb.getLong(); return new UUID(firstLong, secondLong); } - protected byte[] stringToBytes(String string) { + public static byte[] stringToBytes(String string) { return string.getBytes(StandardCharsets.UTF_8); } - protected String bytesToString(byte[] data) { + public static String bytesToString(byte[] data) { return new String(data, StandardCharsets.UTF_8); } - protected static byte[] longToBytes(long x) { + public static byte[] longToBytes(long x) { ByteBuffer longBuffer = ByteBuffer.allocate(Long.BYTES); longBuffer.putLong(0, x); return longBuffer.array(); } - protected static long bytesToLong(byte[] bytes) { + public static long bytesToLong(byte[] bytes) { return ByteBuffer.wrap(bytes).getLong(); } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/TbProtoQueueMsg.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/TbProtoQueueMsg.java index fb7d086daa..6bb72a65fa 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/TbProtoQueueMsg.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/TbProtoQueueMsg.java @@ -50,6 +50,7 @@ public class TbProtoQueueMsg i @Override public byte[] getData() { - return value.toByteArray(); + return value != null ? value.toByteArray() : null; } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index 91ba63427b..d768648aea 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -556,7 +556,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi .stopWhenRead(true) .clientId("monolith-calculated-field-state-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet()) .groupId(topicService.buildTopicName("monolith-calculated-field-state-consumer")) - .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), CalculatedFieldStateProto.parseFrom(msg.getData()), msg.getHeaders())) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), msg.getData() != null ? CalculatedFieldStateProto.parseFrom(msg.getData()) : null, msg.getHeaders())) .admin(cfStateAdmin) .statsService(consumerStatsService) .build(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index d4076ba67d..45a2290f52 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -348,7 +348,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { .stopWhenRead(true) .clientId("tb-rule-engine-calculated-field-state-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet()) .groupId(topicService.buildTopicName("tb-rule-engine-calculated-field-state-consumer")) - .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), CalculatedFieldStateProto.parseFrom(msg.getData()), msg.getHeaders())) + .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), msg.getData() != null ? CalculatedFieldStateProto.parseFrom(msg.getData()) : null, msg.getHeaders())) .admin(cfStateAdmin) .statsService(consumerStatsService) .build(); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index e1aa7c2ee5..783ae6be5c 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -36,12 +36,14 @@ import { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { catchError, filter, switchMap, tap } from 'rxjs/operators'; import { + ArgumentType, CalculatedField, CalculatedFieldEventArguments, CalculatedFieldDebugDialogData, CalculatedFieldDialogData, CalculatedFieldTestScriptDialogData, getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights, } from '@shared/models/calculated-field.models'; import { CalculatedFieldDebugDialogComponent, @@ -83,7 +85,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.fetchCalculatedFields(pageLink); - this.addEntity = this.addCalculatedField.bind(this); + this.addEntity = this.getCalculatedFieldDialog.bind(this); this.deleteEntityTitle = (field: CalculatedField) => this.translate.instant('calculated-fields.delete-title', {title: field.name}); this.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text'); this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count}); @@ -122,7 +124,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig true, onAction: (_, entity) => this.openDebugEventsDialog(entity), }, @@ -179,20 +181,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { - return this.getCalculatedFieldDialog() - .pipe( - filter(Boolean), - switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField({ entityId: this.entityId, ...calculatedField })), - ) - } - private editCalculatedField(calculatedField: CalculatedField, isDirty = false): void { this.getCalculatedFieldDialog(calculatedField, 'action.apply', isDirty) - .pipe( - filter(Boolean), - switchMap((updatedCalculatedField) => this.calculatedFieldsService.saveCalculatedField({ ...calculatedField, ...updatedCalculatedField })), - ) .subscribe((res) => { if (res) { this.updateData(); @@ -217,7 +207,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { - acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key) ? argumentsObj[key] : ''; + const type = calculatedField.configuration.arguments[key].refEntityKey.type; + acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key) + ? { ...argumentsObj[key], type } + : type === ArgumentType.Rolling ? { values: [], type } : { value: '', type, ts: new Date().getTime() }; return acc; }, {}); return this.dialog.open(CalculatedFieldScriptTestDialogComponent, @@ -282,6 +276,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig @if (group.get('refEntityId')?.get('id')?.value) { - + {{ entityTypeTranslations.get(group.get('refEntityId').get('entityType').value)?.type | translate }} @@ -41,12 +41,13 @@ + [entityType]="group.get('refEntityId').get('entityType').value" + /> } @else { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss index 28bed75ef7..321cde8dfe 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss @@ -40,9 +40,13 @@ } :host ::ng-deep { - .tb-inline-field { + .entity-field { a { font-size: 14px; + white-space: nowrap; + display: block; + overflow: hidden; + text-overflow: ellipsis; } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts index 3c51dc7f79..c82a98eaac 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts @@ -33,7 +33,6 @@ import { NG_VALUE_ACCESSOR, ValidationErrors, Validator, - Validators } from '@angular/forms'; import { ArgumentEntityType, @@ -49,8 +48,7 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; -import { isDefinedAndNotNull } from '@core/utils'; -import { charsWithNumRegex } from '@shared/models/regex.constants'; +import { isDefined, isDefinedAndNotNull } from '@core/utils'; import { TbPopoverComponent } from '@shared/components/popover.component'; @Component({ @@ -143,10 +141,10 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces buttonTitle: this.argumentsFormArray.at(index)?.value ? 'action.apply' : 'action.add', tenantId: this.tenantId, entityName: this.entityName, - usedArgumentNames: this.argumentsFormArray.value.map(({ argumentName }) => argumentName).filter(name => name !== argumentObj.argumentName), + usedArgumentNames: this.argumentsFormArray.getRawValue().map(({ argumentName }) => argumentName).filter(name => name !== argumentObj.argumentName), }; this.popoverComponent = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, CalculatedFieldArgumentPanelComponent, 'left', false, null, + this.viewContainerRef, CalculatedFieldArgumentPanelComponent, isDefined(index) ? 'left' : 'right', false, null, ctx, {}, {}, {}, true); @@ -201,7 +199,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces private getArgumentFormGroup(value: CalculatedFieldArgumentValue): FormGroup { return this.fb.group({ ...value, - argumentName: [value.argumentName, [Validators.required, Validators.maxLength(255), Validators.pattern(charsWithNumRegex)]], + argumentName: [{ value: value.argumentName, disabled: true }], ...(value.refEntityId ? { refEntityId: this.fb.group({ entityType: [{ value: value.refEntityId.entityType, disabled: true }], diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index f1e6905aab..20fba95688 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -96,9 +96,11 @@ required formControlName="expressionSCRIPT" functionName="calculate" + class="expression-edit" [functionArgs]="functionArgs$ | async" [disableUndefinedCheck]="true" [scriptLanguage]="ScriptLanguage.TBEL" + [highlightRules]="argumentsHighlightRules$ | async" [editorCompleter]="argumentsEditorCompleter$ | async" helpId="calculated-field/expression_fn" > @@ -183,7 +185,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss index bc49e05e8d..c17dbf8bb5 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss @@ -20,3 +20,21 @@ max-width: 100%; } } + +:host ::ng-deep { + .expression-edit { + .ace_tb { + &.ace_calculated-field { + &-key { + color: #C52F00; + } + &-ts, &-time-window, &-values, &-value { + color: #7214D0; + } + &-start-ts, &-end-ts, &-limit { + color: #185F2A; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index a0e439029d..bffb405534 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { AfterViewInit, Component, Inject } from '@angular/core'; +import { Component, DestroyRef, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -28,6 +28,7 @@ import { CalculatedFieldType, CalculatedFieldTypeTranslations, getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights, OutputType, OutputTypeTranslations } from '@shared/models/calculated-field.models'; @@ -37,13 +38,14 @@ import { EntityType } from '@shared/models/entity-type.models'; import { map, startWith } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ScriptLanguage } from '@shared/models/rule-node.models'; +import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; @Component({ selector: 'tb-calculated-field-dialog', templateUrl: './calculated-field-dialog.component.html', styleUrls: ['./calculated-field-dialog.component.scss'], }) -export class CalculatedFieldDialogComponent extends DialogComponent implements AfterViewInit { +export class CalculatedFieldDialogComponent extends DialogComponent { fieldFormGroup = this.fb.group({ name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]], @@ -73,6 +75,12 @@ export class CalculatedFieldDialogComponent extends DialogComponent getCalculatedFieldArgumentsEditorCompleter(argumentsObj)) ); + argumentsHighlightRules$ = this.configFormGroup.get('arguments').valueChanges + .pipe( + startWith(this.data.value?.configuration?.arguments ?? {}), + map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) + ); + additionalDebugActionConfig = this.data.value?.id ? { ...this.data.additionalDebugActionConfig, action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }), @@ -92,8 +100,11 @@ export class CalculatedFieldDialogComponent extends DialogComponent, + private calculatedFieldsService: CalculatedFieldsService, + private destroyRef: DestroyRef, private fb: FormBuilder) { super(store, router, dialogRef); + this.observeIsLoading(); this.applyDialogData(); this.observeTypeChanges(); } @@ -112,19 +123,15 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.dialogRef.close(calculatedField)); } } @@ -169,4 +176,19 @@ export class CalculatedFieldDialogComponent extends DialogComponent { + if (loading) { + this.fieldFormGroup.disable({emitEvent: false}); + } else { + this.fieldFormGroup.enable({emitEvent: false}); + this.toggleScopeByOutputType(this.outputFormGroup.get('type').value); + this.toggleKeyByCalculatedFieldType(this.fieldFormGroup.get('type').value); + if (this.data.isDirty) { + this.fieldFormGroup.markAsDirty(); + } + } + }); + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html index c2fe831204..039df61fc6 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html @@ -108,7 +108,14 @@ @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Attribute) {
{{ 'calculated-fields.timeseries-key' | translate }}
- + @if (refEntityKeyFormGroup.get('type').value === ArgumentType.LatestTelemetry) { + + } @else { + + } + + +
} @else { @if (enableAttributeScopeSelection) { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.html index eea3523d00..3f5c864708 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.html @@ -19,16 +19,38 @@
{{ 'calculated-fields.arguments' | translate }}
-
{{ 'calculated-fields.argument-name' | translate }}
-
{{ 'common.value' | translate }}
+
{{ 'common.name' | translate }}
+
{{ 'common.type' | translate }}
+
{{ 'common.data' | translate }}
@for (group of argumentsFormArray.controls; track group) {
- + - + + + + {{ ArgumentTypeTranslations.get(argumentsTypeMap.get(group.get('argumentName').value)) | translate }} + + + +
+ @if (argumentsTypeMap.get(group.get('argumentName').value) === ArgumentType.Rolling) { + + + + } @else { + + + + + } + +
}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.scss index 1b2c8670c1..19046fa254 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.scss @@ -15,14 +15,18 @@ */ @use '../../../../../../../scss/constants' as constants; +:host { + .tb-form-table { + min-width: 700px; + } +} + :host::ng-deep { .tb-form-table-row { .argument-value { .tb-value-type.row { - @media #{constants.$mat-lt-sm} { - width: 100px; - min-width: 100px; - } + width: 120px; + min-width: 120px; } } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.ts index c8c9f4e778..2411a7fd81 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef } from '@angular/core'; +import { Component, forwardRef, Input } from '@angular/core'; import { ControlValueAccessor, NG_VALIDATORS, @@ -26,6 +26,22 @@ import { } from '@angular/forms'; import { PageComponent } from '@shared/components/page.component'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { entityTypeTranslations } from '@shared/models/entity-type.models'; +import { + ArgumentType, + ArgumentTypeTranslations, + CalculatedFieldArgumentEventValue, + CalculatedFieldRollingTelemetryArgumentValue, + CalculatedFieldSingleArgumentValue, + CalculatedFieldEventArguments, + CalculatedFieldType +} from '@shared/models/calculated-field.models'; +import { + JsonObjectEditDialogComponent, + JsonObjectEditDialogData +} from '@shared/components/dialog/json-object-edit-dialog.component'; +import { filter } from 'rxjs/operators'; +import { MatDialog } from '@angular/material/dialog'; @Component({ selector: 'tb-calculated-field-test-arguments', @@ -46,32 +62,39 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; }) export class CalculatedFieldTestArgumentsComponent extends PageComponent implements ControlValueAccessor, Validator { + @Input() argumentsTypeMap: Map; argumentsFormArray = this.fb.array([]); - private propagateChange: (value: { argumentName: string; value: unknown }) => void; + readonly entityTypeTranslations = entityTypeTranslations; + readonly ArgumentTypeTranslations = ArgumentTypeTranslations; + readonly ArgumentType = ArgumentType; + readonly CalculatedFieldType = CalculatedFieldType; - constructor(private fb: FormBuilder) { + private propagateChange: (value: CalculatedFieldEventArguments) => void = () => {}; + + constructor(private fb: FormBuilder, private dialog: MatDialog) { super(); this.argumentsFormArray.valueChanges .pipe(takeUntilDestroyed()) .subscribe(() => this.propagateChange(this.getValue())); } - registerOnChange(propagateChange: (value: { argumentName: string; value: unknown }) => void): void { + registerOnChange(propagateChange: (value: CalculatedFieldEventArguments) => void): void { this.propagateChange = propagateChange; } registerOnTouched(_): void { } - writeValue(argumentsObj: Record): void { + writeValue(argumentsObj: CalculatedFieldEventArguments): void { this.argumentsFormArray.clear(); Object.keys(argumentsObj).forEach(key => { - this.argumentsFormArray.push(this.fb.group({ - argumentName: [{ value: key, disabled: true}], - value: [argumentsObj[key]] - }) as FormGroup, {emitEvent: false}); + const value = { ...argumentsObj[key], argumentName: key } as CalculatedFieldArgumentEventValue; + this.argumentsFormArray.push(this.argumentsTypeMap.get(key) === ArgumentType.Rolling + ? this.getRollingArgumentFormGroup(value as CalculatedFieldRollingTelemetryArgumentValue) + : this.getSimpleArgumentFormGroup(value as CalculatedFieldSingleArgumentValue) + ); }); } @@ -79,11 +102,46 @@ export class CalculatedFieldTestArgumentsComponent extends PageComponent impleme return this.argumentsFormArray.valid ? null : { arguments: { valid: false } }; } - private getValue(): { argumentName: string; value: unknown } { + openEditJSONDialog(group: FormGroup): void { + this.dialog.open(JsonObjectEditDialogComponent, { + disableClose: true, + height: '760px', + maxHeight: '70vh', + minWidth: 'min(700px, 100%)', + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + jsonValue: group.value, + required: true, + fillHeight: true + } + }).afterClosed() + .pipe(filter(Boolean)) + .subscribe(result => this.argumentsTypeMap.get(group.get('argumentName').value) === ArgumentType.Rolling + ? group.patchValue({ timeWindow: (result as CalculatedFieldRollingTelemetryArgumentValue).timeWindow, values: (result as CalculatedFieldRollingTelemetryArgumentValue).values }) + : group.patchValue({ ts: (result as CalculatedFieldSingleArgumentValue).ts, value: (result as CalculatedFieldSingleArgumentValue).value }) ); + } + + private getSimpleArgumentFormGroup({ argumentName, ts, value }: CalculatedFieldSingleArgumentValue): FormGroup { + return this.fb.group({ + argumentName: [{ value: argumentName, disabled: true}], + ts: [ts], + value: [value] + }) as FormGroup; + } + + private getRollingArgumentFormGroup({ argumentName, timeWindow, values }: CalculatedFieldRollingTelemetryArgumentValue): FormGroup { + return this.fb.group({ + timeWindow: [timeWindow ?? {}], + argumentName: [{ value: argumentName, disabled: true }], + values: [values] + }) as FormGroup; + } + + private getValue(): CalculatedFieldEventArguments { return this.argumentsFormArray.getRawValue().reduce((acc, rowItem) => { - const { argumentName, value } = rowItem; + const { argumentName, ...value } = rowItem; acc[argumentName] = value; return acc; - }, {}) as { argumentName: string; value: unknown }; + }, {}); } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html index 3f35ecbc0c..789d290a6d 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html @@ -36,9 +36,11 @@ #expressionContent formControlName="expression" functionName="calculate" + class="expression-edit" [functionArgs]="functionArgs" [disableUndefinedCheck]="true" [fillHeight]="true" + [highlightRules]="data.argumentsHighlightRules" [scriptLanguage]="ScriptLanguage.TBEL" [editorCompleter]="data.argumentsEditorCompleter" resultType="object" @@ -52,7 +54,7 @@
{{ 'calculated-fields.arguments' | translate }}
- +
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.scss index eaee8e443d..03ef1c3540 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.scss @@ -71,4 +71,20 @@ background-image: url("../../../../../../../assets/split.js/grips/vertical.png"); } } + + .expression-edit { + .ace_tb { + &.ace_calculated-field { + &-key { + color: #C52F00; + } + &-ts, &-time-window, &-values, &-value { + color: #7214D0; + } + &-start-ts, &-end-ts, &-limit { + color: #185F2A; + } + } + } + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts index 83e3625219..769387b3dd 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts @@ -38,7 +38,12 @@ import { beautifyJs } from '@shared/models/beautify.models'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { filter } from 'rxjs/operators'; -import { CalculatedFieldTestScriptDialogData } from '@shared/models/calculated-field.models'; +import { + ArgumentType, + CalculatedFieldEventArguments, + CalculatedFieldTestScriptDialogData, + TestArgumentTypeMap +} from '@shared/models/calculated-field.models'; @Component({ selector: 'tb-calculated-field-script-test-dialog', @@ -61,6 +66,7 @@ export class CalculatedFieldScriptTestDialogComponent extends DialogComponent(); readonly ContentType = ContentType; readonly ScriptLanguage = ScriptLanguage; @@ -81,7 +87,7 @@ export class CalculatedFieldScriptTestDialogComponent extends DialogComponent this.calculatedFieldScriptTestFormGroup.get('expression').patchValue(res, {emitEvent: false}) ); - this.calculatedFieldScriptTestFormGroup.get('arguments').patchValue(this.data.arguments, {emitEvent: false}); + this.calculatedFieldScriptTestFormGroup.get('arguments').patchValue(this.getArgumentsValue()); } ngAfterViewInit(): void { @@ -117,7 +123,7 @@ export class CalculatedFieldScriptTestDialogComponent extends DialogComponent { if (result.error) { @@ -157,6 +163,26 @@ export class CalculatedFieldScriptTestDialogComponent extends DialogComponent { + acc[key] = argumentsValue[key]; + acc[key].type = TestArgumentTypeMap.get(this.argumentsTypeMap.get(key)); + return acc; + }, {}); + } + + private getArgumentsValue(): CalculatedFieldEventArguments { + return Object.keys(this.data.arguments) + .reduce((acc, key) => { + const { type, ...argumentObj } = this.data.arguments[key]; + this.argumentsTypeMap.set(key, type); + acc[key] = argumentObj; + return acc; + }, {}); + } + private initSplitLayout(smallMode = false): void { const [leftPanel, rightPanel, topRightPanel, bottomRightPanel] = [ this.leftPanelElmRef.nativeElement, diff --git a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html index 9cce511a6b..147c7ed130 100644 --- a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+

{{ title }}

@@ -25,30 +25,26 @@ close
- - -
-
-
- - -
+
+
+ +
diff --git a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts index a0ffb6b373..d1bc76e194 100644 --- a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts @@ -30,6 +30,7 @@ export interface JsonObjectEditDialogData { title?: string; saveLabel?: string; cancelLabel?: string; + fillHeight?: boolean; } @Component({ diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts index 3f5417c2d0..b4898b2377 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.ts +++ b/ui-ngx/src/app/shared/components/js-func.component.ts @@ -184,9 +184,12 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal this.updateFunctionArgsString(); this.updateFunctionLabel(); } - if (changes.editorCompleter) { + if (changes.editorCompleter?.previousValue) { this.updateCompleters(); } + if (changes.highlightRules?.previousValue) { + this.updateHighlightRules(); + } } ngOnInit(): void { @@ -247,21 +250,7 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal } }); } - // @ts-ignore - if (!!this.highlightRules && !!this.jsEditor.session.$mode) { - // @ts-ignore - const newMode = new this.jsEditor.session.$mode.constructor(); - newMode.$highlightRules = new newMode.HighlightRules(); - for(const group in this.highlightRules) { - if(!!newMode.$highlightRules.$rules[group]) { - newMode.$highlightRules.$rules[group].unshift(...this.highlightRules[group]); - } else { - newMode.$highlightRules.$rules[group] = this.highlightRules[group]; - } - } - // @ts-ignore - this.jsEditor.session.$onChangeMode(newMode); - } + this.updateHighlightRules(); this.updateJsWorkerGlobals(); this.initialCompleters = this.jsEditor.completers || []; this.updateCompleters(); @@ -282,6 +271,24 @@ export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlVal } } + private updateHighlightRules(): void { + // @ts-ignore + if (!!this.highlightRules && !!this.jsEditor.session.$mode) { + // @ts-ignore + const newMode = new this.jsEditor.session.$mode.constructor(); + newMode.$highlightRules = new newMode.HighlightRules(); + for(const group in this.highlightRules) { + if(!!newMode.$highlightRules.$rules[group]) { + newMode.$highlightRules.$rules[group].unshift(...this.highlightRules[group]); + } else { + newMode.$highlightRules.$rules[group] = this.highlightRules[group]; + } + } + // @ts-ignore + this.jsEditor.session.$onChangeMode(newMode); + } + } + private onAceEditorResize() { if (this.editorsResizeCaf) { this.editorsResizeCaf(); diff --git a/ui-ngx/src/app/shared/components/value-input.component.html b/ui-ngx/src/app/shared/components/value-input.component.html index a7108f9174..57aed608c5 100644 --- a/ui-ngx/src/app/shared/components/value-input.component.html +++ b/ui-ngx/src/app/shared/components/value-input.component.html @@ -93,7 +93,7 @@ warning -
diff --git a/ui-ngx/src/app/shared/components/value-input.component.ts b/ui-ngx/src/app/shared/components/value-input.component.ts index 93a5a6c8bf..3065dd17c4 100644 --- a/ui-ngx/src/app/shared/components/value-input.component.ts +++ b/ui-ngx/src/app/shared/components/value-input.component.ts @@ -85,6 +85,10 @@ export class ValueInputComponent implements OnInit, OnDestroy, OnChanges, Contro @coerceBoolean() required = true; + @Input() + @coerceBoolean() + hideJsonEdit = false; + @Input() layout: ValueInputLayout | Layout = 'row'; diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index a718b236bf..5ce1a50d04 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -28,6 +28,7 @@ import { EntityType } from '@shared/models/entity-type.models'; import { AliasFilterType } from '@shared/models/alias.models'; import { Observable } from 'rxjs'; import { TbEditorCompleter } from '@shared/models/ace/completion.models'; +import { AceHighlightRules } from '@shared/models/ace/ace.models'; export interface CalculatedField extends Omit, 'label'>, HasVersion, HasTenantId, ExportableEntity { debugSettings?: EntityDebugSettings; @@ -85,6 +86,19 @@ export enum ArgumentType { Rolling = 'TS_ROLLING', } +export enum TestArgumentType { + Single = 'SINGLE_VALUE', + Rolling = 'TS_ROLLING', +} + +export const TestArgumentTypeMap = new Map( + [ + [ArgumentType.Attribute, TestArgumentType.Single], + [ArgumentType.LatestTelemetry, TestArgumentType.Single], + [ArgumentType.Rolling, TestArgumentType.Rolling], + ] +) + export enum OutputType { Attribute = 'ATTRIBUTES', Timeseries = 'TIME_SERIES', @@ -149,12 +163,13 @@ export interface CalculatedFieldDebugDialogData { } export interface CalculatedFieldTestScriptInputParams { - arguments: Record, + arguments: CalculatedFieldEventArguments; expression: string; } export interface CalculatedFieldTestScriptDialogData extends CalculatedFieldTestScriptInputParams { - argumentsEditorCompleter: TbEditorCompleter + argumentsEditorCompleter: TbEditorCompleter; + argumentsHighlightRules: AceHighlightRules; openCalculatedFieldEdit?: boolean; } @@ -189,18 +204,23 @@ export const getCalculatedFieldCurrentEntityFilter = (entityName: string, entity } } -export interface CalculatedFieldAttributeArgumentValue { +export interface CalculatedFieldArgumentValueBase { + argumentName: string; + type: ArgumentType; +} + +export interface CalculatedFieldAttributeArgumentValue extends CalculatedFieldArgumentValueBase { ts: number; value: ValueType; } -export interface CalculatedFieldLatestTelemetryArgumentValue { +export interface CalculatedFieldLatestTelemetryArgumentValue extends CalculatedFieldArgumentValueBase { ts: number; value: ValueType; } -export interface CalculatedFieldRollingTelemetryArgumentValue { - timewindow: { startTs: number; endTs: number; limit: number }; +export interface CalculatedFieldRollingTelemetryArgumentValue extends CalculatedFieldArgumentValueBase { + timeWindow: { startTs: number; endTs: number; limit: number }; values: CalculatedFieldSingleArgumentValue[]; } @@ -248,7 +268,7 @@ export const CalculatedFieldAttributeValueArgumentAutocomplete = { export const CalculatedFieldRollingValueArgumentAutocomplete = { meta: 'object', - type: '{ values: { ts: number; value: any; }[]; timewindow: { startTs: number; endTs: number; limit: number } }; }', + type: '{ values: { ts: number; value: any; }[]; timeWindow: { startTs: number; endTs: number; limit: number } }; }', description: 'Calculated field rolling value argument.', children: { values: { @@ -256,7 +276,7 @@ export const CalculatedFieldRollingValueArgumentAutocomplete = { type: '{ ts: number; value: any; }[]', description: 'Values array', }, - timewindow: { + timeWindow: { meta: 'object', type: '{ startTs: number; endTs: number; limit: number }', description: 'Time window configuration', @@ -295,5 +315,72 @@ export const getCalculatedFieldArgumentsEditorCompleter = (argumentsObj: Record< break; } return acc; - }, {})) + }, {})); +} + +export const getCalculatedFieldArgumentsHighlights = ( + argumentsObj: Record +): AceHighlightRules => { + return { + start: Object.keys(argumentsObj).map(key => ({ + token: 'tb.calculated-field-key', + regex: `\\b${key}\\b`, + next: argumentsObj[key].refEntityKey.type === ArgumentType.Rolling + ? 'calculatedFieldRollingArgumentValue' + : 'calculatedFieldSingleArgumentValue' + })), + ...calculatedFieldSingleArgumentValueHighlightRules, + ...calculatedFieldRollingArgumentValueHighlightRules, + ...calculatedFieldTimeWindowArgumentValueHighlightRules + }; +}; + +const calculatedFieldSingleArgumentValueHighlightRules: AceHighlightRules = { + calculatedFieldSingleArgumentValue: [ + { + token: 'tb.calculated-field-value', + regex: /value/, + next: 'no_regex' + }, + { + token: 'tb.calculated-field-ts', + regex: /ts/, + next: 'no_regex' + } + ], +} + +const calculatedFieldRollingArgumentValueHighlightRules: AceHighlightRules = { + calculatedFieldRollingArgumentValue: [ + { + token: 'tb.calculated-field-values', + regex: /values/, + next: 'no_regex' + }, + { + token: 'tb.calculated-field-time-window', + regex: /timeWindow/, + next: 'calculatedFieldRollingArgumentTimeWindow' + } + ], +} + +const calculatedFieldTimeWindowArgumentValueHighlightRules: AceHighlightRules = { + calculatedFieldRollingArgumentTimeWindow: [ + { + token: 'tb.calculated-field-start-ts', + regex: /startTs/, + next: 'no_regex' + }, + { + token: 'tb.calculated-field-end-ts', + regex: /endTs/, + next: 'no_regex' + }, + { + token: 'tb.calculated-field-limit', + regex: /limit/, + next: 'no_regex' + } + ] } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 4822d23387..4d19432fe7 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1100,6 +1100,7 @@ "general": "General", "username": "Username", "password": "Password", + "data": "Data", "enter-username": "Enter username", "enter-password": "Enter password", "enter-search": "Enter search",