Merge branch 'feature/calculated-fields' of github.com:thingsboard/thingsboard into calculated-fields
This commit is contained in:
commit
b49abaa523
@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -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<CalculatedFieldStateProto> msg : msgs) {
|
||||
try {
|
||||
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<CalculatedFieldStateProto> 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();
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -50,6 +50,7 @@ public class TbProtoQueueMsg<T extends com.google.protobuf.GeneratedMessageV3> i
|
||||
|
||||
@Override
|
||||
public byte[] getData() {
|
||||
return value.toByteArray();
|
||||
return value != null ? value.toByteArray() : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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<CalculatedFie
|
||||
this.entityTranslations = entityTypeTranslations.get(EntityType.CALCULATED_FIELD);
|
||||
|
||||
this.entitiesFetchFunction = (pageLink: PageLink) => 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<CalculatedFie
|
||||
},
|
||||
{
|
||||
name: this.translate.instant('entity-view.events'),
|
||||
icon: 'history',
|
||||
icon: 'mdi:clipboard-text-clock',
|
||||
isEnabled: () => true,
|
||||
onAction: (_, entity) => this.openDebugEventsDialog(entity),
|
||||
},
|
||||
@ -179,20 +181,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
|
||||
}
|
||||
}
|
||||
|
||||
private addCalculatedField(): Observable<CalculatedField> {
|
||||
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<CalculatedFie
|
||||
},
|
||||
enterAnimationDuration: isDirty ? 0 : null,
|
||||
})
|
||||
.afterClosed();
|
||||
.afterClosed()
|
||||
.pipe(filter(Boolean));
|
||||
}
|
||||
|
||||
private openDebugEventsDialog(calculatedField: CalculatedField): void {
|
||||
@ -271,7 +262,10 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
|
||||
|
||||
private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true): Observable<string> {
|
||||
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, CalculatedFieldTestScriptDialogData, string>(CalculatedFieldScriptTestDialogComponent,
|
||||
@ -282,6 +276,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
|
||||
arguments: resultArguments,
|
||||
expression: calculatedField.configuration.expression,
|
||||
argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments),
|
||||
argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments),
|
||||
openCalculatedFieldEdit
|
||||
}
|
||||
}).afterClosed()
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
<section class="datasource-field flex w-1/3 gap-2 xs:hidden">
|
||||
@if (group.get('refEntityId')?.get('id')?.value) {
|
||||
<ng-container [formGroup]="group.get('refEntityId')">
|
||||
<mat-form-field appearance="outline" class="tb-inline-field flex-1" subscriptSizing="dynamic">
|
||||
<mat-form-field appearance="outline" class="tb-inline-field w-1/2" subscriptSizing="dynamic">
|
||||
<mat-select [value]="group.get('refEntityId').get('entityType').value" formControlName="entityType">
|
||||
<mat-option [value]="group.get('refEntityId').get('entityType').value">
|
||||
{{ entityTypeTranslations.get(group.get('refEntityId').get('entityType').value)?.type | translate }}
|
||||
@ -41,12 +41,13 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<tb-entity-autocomplete
|
||||
class="flex-1"
|
||||
class="entity-field w-1/2"
|
||||
formControlName="id"
|
||||
[inlineField]="true"
|
||||
[hideLabel]="true"
|
||||
[placeholder]="'action.set' | translate"
|
||||
[entityType]="group.get('refEntityId').get('entityType').value"/>
|
||||
[entityType]="group.get('refEntityId').get('entityType').value"
|
||||
/>
|
||||
</ng-container>
|
||||
} @else {
|
||||
<mat-form-field appearance="outline" class="tb-inline-field flex-1" subscriptSizing="dynamic">
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 }],
|
||||
|
||||
@ -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 @@
|
||||
</button>
|
||||
<button mat-raised-button color="primary"
|
||||
(click)="add()"
|
||||
[disabled]="fieldFormGroup.invalid || !fieldFormGroup.dirty">
|
||||
[disabled]="(isLoading$ | async) || fieldFormGroup.invalid || !fieldFormGroup.dirty">
|
||||
{{ data.buttonTitle | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<CalculatedFieldDialogComponent, CalculatedField> implements AfterViewInit {
|
||||
export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFieldDialogComponent, CalculatedField> {
|
||||
|
||||
fieldFormGroup = this.fb.group({
|
||||
name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]],
|
||||
@ -73,6 +75,12 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
|
||||
map(argumentsObj => 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<CalculatedFi
|
||||
protected router: Router,
|
||||
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDialogData,
|
||||
protected dialogRef: MatDialogRef<CalculatedFieldDialogComponent, CalculatedField>,
|
||||
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<CalculatedFi
|
||||
return { configuration: { ...restConfig, type, expression: configuration['expression'+type] }, ...rest, type } as CalculatedField;
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (this.data.isDirty) {
|
||||
this.fieldFormGroup.markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.dialogRef.close(null);
|
||||
}
|
||||
|
||||
add(): void {
|
||||
if (this.fieldFormGroup.valid) {
|
||||
this.dialogRef.close(this.fromGroupValue);
|
||||
this.calculatedFieldsService.saveCalculatedField({ entityId: this.data.entityId, ...(this.data.value ?? {}), ...this.fromGroupValue})
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(calculatedField => this.dialogRef.close(calculatedField));
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,4 +176,19 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
|
||||
this.configFormGroup.get('expressionSCRIPT').enable({emitEvent: false});
|
||||
}
|
||||
}
|
||||
|
||||
private observeIsLoading(): void {
|
||||
this.isLoading$.pipe(takeUntilDestroyed()).subscribe(loading => {
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,7 +108,14 @@
|
||||
@if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Attribute) {
|
||||
<div class="tb-form-row">
|
||||
<div class="fixed-title-width tb-required">{{ 'calculated-fields.timeseries-key' | translate }}</div>
|
||||
@if (refEntityKeyFormGroup.get('type').value === ArgumentType.LatestTelemetry) {
|
||||
<ng-container [ngTemplateOutlet]="timeseriesKeyAutocomplete"/>
|
||||
} @else {
|
||||
<ng-container [ngTemplateOutlet]="timeseriesKeyAutocomplete"/>
|
||||
}
|
||||
<ng-template #timeseriesKeyAutocomplete>
|
||||
<tb-entity-key-autocomplete class="flex-1" formControlName="key" [dataKeyType]="DataKeyType.timeseries" [entityFilter]="entityFilter"/>
|
||||
</ng-template>
|
||||
</div>
|
||||
} @else {
|
||||
@if (enableAttributeScopeSelection) {
|
||||
|
||||
@ -19,16 +19,38 @@
|
||||
<div>{{ 'calculated-fields.arguments' | translate }}</div>
|
||||
<div class="tb-form-table">
|
||||
<div class="tb-form-table-header">
|
||||
<div class="tb-form-table-header-cell w-1/4" tbTruncateWithTooltip>{{ 'calculated-fields.argument-name' | translate }}</div>
|
||||
<div class="tb-form-table-header-cell flex-1">{{ 'common.value' | translate }}</div>
|
||||
<div class="tb-form-table-header-cell w-1/6">{{ 'common.name' | translate }}</div>
|
||||
<div class="tb-form-table-header-cell w-1/5 xs:hidden">{{ 'common.type' | translate }}</div>
|
||||
<div class="tb-form-table-header-cell flex-1">{{ 'common.data' | translate }}</div>
|
||||
</div>
|
||||
<div class="tb-form-table-body">
|
||||
@for (group of argumentsFormArray.controls; track group) {
|
||||
<div [formGroup]="group" class="tb-form-table-row">
|
||||
<mat-form-field appearance="outline" class="tb-inline-field w-1/4" subscriptSizing="dynamic">
|
||||
<mat-form-field appearance="outline" class="tb-inline-field w-1/6" subscriptSizing="dynamic">
|
||||
<input matInput formControlName="argumentName" placeholder="{{ 'action.set' | translate }}">
|
||||
</mat-form-field>
|
||||
<tb-value-input class="argument-value flex-1" [required]="false" [shortBooleanField]="true" formControlName="value"/>
|
||||
<mat-form-field appearance="outline" class="tb-inline-field w-1/5 xs:hidden" subscriptSizing="dynamic">
|
||||
<mat-select [value]="argumentsTypeMap.get(group.get('argumentName').value)" [disabled]="true">
|
||||
<mat-option [value]="argumentsTypeMap.get(group.get('argumentName').value)">
|
||||
{{ ArgumentTypeTranslations.get(argumentsTypeMap.get(group.get('argumentName').value)) | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div class="flex flex-1 items-center gap-2">
|
||||
@if (argumentsTypeMap.get(group.get('argumentName').value) === ArgumentType.Rolling) {
|
||||
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-inline-field flex-1">
|
||||
<input matInput tb-json-to-string name="values" formControlName="values" placeholder="{{ 'value.json-value' | translate }}*"/>
|
||||
</mat-form-field>
|
||||
} @else {
|
||||
<mat-form-field appearance="outline" class="tb-inline-field w-1/3" subscriptSizing="dynamic">
|
||||
<input matInput formControlName="ts" type="number" placeholder="{{ 'action.set' | translate }}">
|
||||
</mat-form-field>
|
||||
<tb-value-input class="argument-value min-w-60 flex-1" [required]="false" [hideJsonEdit]="true" [shortBooleanField]="true" formControlName="value"/>
|
||||
}
|
||||
<button mat-icon-button class="tb-mat-32" (click)="openEditJSONDialog(group)">
|
||||
<mat-icon class="tb-mat-20">open_in_new</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<string, ArgumentType>;
|
||||
|
||||
argumentsFormArray = this.fb.array<FormGroup>([]);
|
||||
|
||||
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<string, unknown>): 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, JsonObjectEditDialogData, CalculatedFieldArgumentEventValue>(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 };
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 @@
|
||||
<div class="block-label-container right-top">
|
||||
<span class="block-label">{{ 'calculated-fields.arguments' | translate }}</span>
|
||||
</div>
|
||||
<tb-calculated-field-test-arguments class="size-full" formControlName="arguments"/>
|
||||
<tb-calculated-field-test-arguments class="size-full" formControlName="arguments" [argumentsTypeMap]="argumentsTypeMap"/>
|
||||
</div>
|
||||
</div>
|
||||
<div #bottomRightPanel class="test-block-content">
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Ca
|
||||
arguments: [],
|
||||
output: []
|
||||
});
|
||||
argumentsTypeMap = new Map<string, ArgumentType>();
|
||||
|
||||
readonly ContentType = ContentType;
|
||||
readonly ScriptLanguage = ScriptLanguage;
|
||||
@ -81,7 +87,7 @@ export class CalculatedFieldScriptTestDialogComponent extends DialogComponent<Ca
|
||||
beautifyJs(this.data.expression, {indent_size: 4}).pipe(filter(Boolean), takeUntilDestroyed()).subscribe(
|
||||
(res) => 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<Ca
|
||||
if (this.checkInputParamErrors()) {
|
||||
return this.calculatedFieldService.testScript({
|
||||
expression: this.calculatedFieldScriptTestFormGroup.get('expression').value,
|
||||
arguments: this.calculatedFieldScriptTestFormGroup.get('arguments').value
|
||||
arguments: this.getTestArguments()
|
||||
}).pipe(
|
||||
switchMap(result => {
|
||||
if (result.error) {
|
||||
@ -157,6 +163,26 @@ export class CalculatedFieldScriptTestDialogComponent extends DialogComponent<Ca
|
||||
this.initSplitLayout(this.testScriptContainer.nativeElement.clientWidth <= 960);
|
||||
}
|
||||
|
||||
private getTestArguments(): CalculatedFieldEventArguments {
|
||||
const argumentsValue = this.calculatedFieldScriptTestFormGroup.get('arguments').value;
|
||||
return Object.keys(argumentsValue)
|
||||
.reduce((acc, key) => {
|
||||
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,
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
limitations under the License.
|
||||
|
||||
-->
|
||||
<form [formGroup]="jsonFormGroup" (ngSubmit)="add()" style="min-width: 400px;">
|
||||
<form [formGroup]="jsonFormGroup" (ngSubmit)="add()" style="min-width: 400px;" class="h-full">
|
||||
<mat-toolbar class="flex flex-row" color="primary">
|
||||
<h2>{{ title }}</h2>
|
||||
<span class="flex-1"></span>
|
||||
@ -25,30 +25,26 @@
|
||||
<mat-icon class="material-icons">close</mat-icon>
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
|
||||
</mat-progress-bar>
|
||||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
|
||||
<div mat-dialog-content>
|
||||
<fieldset [disabled]="isLoading$ | async">
|
||||
<div style="height: 4px;"></div>
|
||||
<div mat-dialog-content class="flex-1">
|
||||
<tb-json-object-edit
|
||||
formControlName="json"
|
||||
class="block h-full"
|
||||
label="{{ 'value.json-value' | translate }}"
|
||||
[jsonRequired]="required"
|
||||
[fillHeight]="false">
|
||||
[fillHeight]="data.fillHeight">
|
||||
</tb-json-object-edit>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div mat-dialog-actions class="flex flex-row items-center justify-end">
|
||||
<span class="flex-1"></span>
|
||||
<button mat-button color="primary"
|
||||
type="button"
|
||||
[disabled]="(isLoading$ | async)"
|
||||
(click)="cancel()" cdkFocusInitial>
|
||||
{{ cancelButtonLabel }}
|
||||
</button>
|
||||
<button mat-button mat-raised-button color="primary"
|
||||
type="submit"
|
||||
[disabled]="(isLoading$ | async) || jsonFormGroup.invalid || !jsonFormGroup.dirty">
|
||||
[disabled]="jsonFormGroup.invalid || !jsonFormGroup.dirty">
|
||||
{{ saveButtonLabel }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -30,6 +30,7 @@ export interface JsonObjectEditDialogData {
|
||||
title?: string;
|
||||
saveLabel?: string;
|
||||
cancelLabel?: string;
|
||||
fillHeight?: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -93,7 +93,7 @@
|
||||
warning
|
||||
</mat-icon>
|
||||
</mat-form-field>
|
||||
<button mat-icon-button class="tb-mat-32" (click)="openEditJSONDialog($event)">
|
||||
<button *ngIf="!hideJsonEdit" mat-icon-button class="tb-mat-32" (click)="openEditJSONDialog($event)">
|
||||
<mat-icon class="tb-mat-20">open_in_new</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -85,6 +85,10 @@ export class ValueInputComponent implements OnInit, OnDestroy, OnChanges, Contro
|
||||
@coerceBoolean()
|
||||
required = true;
|
||||
|
||||
@Input()
|
||||
@coerceBoolean()
|
||||
hideJsonEdit = false;
|
||||
|
||||
@Input()
|
||||
layout: ValueInputLayout | Layout = 'row';
|
||||
|
||||
|
||||
@ -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<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasTenantId, ExportableEntity<CalculatedFieldId> {
|
||||
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, TestArgumentType>(
|
||||
[
|
||||
[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<string, unknown>,
|
||||
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<ValueType = unknown> {
|
||||
export interface CalculatedFieldArgumentValueBase {
|
||||
argumentName: string;
|
||||
type: ArgumentType;
|
||||
}
|
||||
|
||||
export interface CalculatedFieldAttributeArgumentValue<ValueType = unknown> extends CalculatedFieldArgumentValueBase {
|
||||
ts: number;
|
||||
value: ValueType;
|
||||
}
|
||||
|
||||
export interface CalculatedFieldLatestTelemetryArgumentValue<ValueType = unknown> {
|
||||
export interface CalculatedFieldLatestTelemetryArgumentValue<ValueType = unknown> extends CalculatedFieldArgumentValueBase {
|
||||
ts: number;
|
||||
value: ValueType;
|
||||
}
|
||||
|
||||
export interface CalculatedFieldRollingTelemetryArgumentValue<ValueType = unknown> {
|
||||
timewindow: { startTs: number; endTs: number; limit: number };
|
||||
export interface CalculatedFieldRollingTelemetryArgumentValue<ValueType = unknown> extends CalculatedFieldArgumentValueBase {
|
||||
timeWindow: { startTs: number; endTs: number; limit: number };
|
||||
values: CalculatedFieldSingleArgumentValue<ValueType>[];
|
||||
}
|
||||
|
||||
@ -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<string, CalculatedFieldArgument>
|
||||
): 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'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1100,6 +1100,7 @@
|
||||
"general": "General",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"data": "Data",
|
||||
"enter-username": "Enter username",
|
||||
"enter-password": "Enter password",
|
||||
"enter-search": "Enter search",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user