committed by
GitHub
89 changed files with 3847 additions and 1059 deletions
@ -0,0 +1,49 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.cf; |
||||
|
|
||||
|
import lombok.Builder; |
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.server.common.data.id.CalculatedFieldId; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.util.CollectionsUtil; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
@Data |
||||
|
@Builder |
||||
|
public final class PropagationCalculatedFieldResult implements CalculatedFieldResult { |
||||
|
|
||||
|
private final List<EntityId> propagationEntityIds; |
||||
|
private final TelemetryCalculatedFieldResult result; |
||||
|
|
||||
|
@Override |
||||
|
public TbMsg toTbMsg(EntityId entityId, List<CalculatedFieldId> cfIds) { |
||||
|
return result.toTbMsg(entityId, cfIds); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String stringValue() { |
||||
|
return result.stringValue(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean isEmpty() { |
||||
|
return CollectionsUtil.isEmpty(propagationEntityIds) || result.isEmpty(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.cf.ctx.state.propagation; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.script.api.tbel.TbelCfArg; |
||||
|
import org.thingsboard.script.api.tbel.TbelCfPropagationArg; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.util.CollectionsUtil; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
@Data |
||||
|
public class PropagationArgumentEntry implements ArgumentEntry { |
||||
|
|
||||
|
private List<EntityId> propagationEntityIds; |
||||
|
|
||||
|
private boolean forceResetPrevious; |
||||
|
|
||||
|
public PropagationArgumentEntry(List<EntityId> propagationEntityIds) { |
||||
|
this.propagationEntityIds = propagationEntityIds; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public ArgumentEntryType getType() { |
||||
|
return ArgumentEntryType.PROPAGATION; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Object getValue() { |
||||
|
return propagationEntityIds; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean updateEntry(ArgumentEntry entry) { |
||||
|
if (!(entry instanceof PropagationArgumentEntry propagationArgumentEntry)) { |
||||
|
throw new IllegalArgumentException("Unsupported argument entry type for propagation argument entry: " + entry.getType()); |
||||
|
} |
||||
|
if (propagationArgumentEntry.isEmpty()) { |
||||
|
propagationEntityIds.clear(); |
||||
|
} else { |
||||
|
propagationEntityIds = propagationArgumentEntry.getPropagationEntityIds(); |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean isEmpty() { |
||||
|
return CollectionsUtil.isEmpty(propagationEntityIds); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public TbelCfArg toTbelCfArg() { |
||||
|
return new TbelCfPropagationArg(propagationEntityIds); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,113 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.cf.ctx.state.propagation; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import com.google.common.util.concurrent.MoreExecutors; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.server.actors.TbActorRef; |
||||
|
import org.thingsboard.server.common.data.cf.CalculatedFieldType; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.Output; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.OutputType; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.service.cf.CalculatedFieldResult; |
||||
|
import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult; |
||||
|
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; |
||||
|
|
||||
|
public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState { |
||||
|
|
||||
|
public PropagationCalculatedFieldState(EntityId entityId) { |
||||
|
super(entityId); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { |
||||
|
this.ctx = ctx; |
||||
|
this.actorCtx = actorCtx; |
||||
|
this.requiredArguments = ctx.getArgNames(); |
||||
|
if (ctx.isApplyExpressionForResolvedArguments()) { |
||||
|
this.tbelExpression = ctx.getTbelExpressions().get(ctx.getExpression()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean isReady() { |
||||
|
if (!super.isReady()) { |
||||
|
return false; |
||||
|
} |
||||
|
ArgumentEntry propagationArg = arguments.get(PROPAGATION_CONFIG_ARGUMENT); |
||||
|
return propagationArg != null && !propagationArg.isEmpty(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public CalculatedFieldType getType() { |
||||
|
return CalculatedFieldType.PROPAGATION; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public ListenableFuture<CalculatedFieldResult> performCalculation(Map<String, ArgumentEntry> updatedArgs, CalculatedFieldCtx ctx) { |
||||
|
ArgumentEntry argumentEntry = arguments.get(PROPAGATION_CONFIG_ARGUMENT); |
||||
|
if (!(argumentEntry instanceof PropagationArgumentEntry propagationArgumentEntry) || propagationArgumentEntry.isEmpty()) { |
||||
|
return Futures.immediateFuture(PropagationCalculatedFieldResult.builder().build()); |
||||
|
} |
||||
|
if (ctx.isApplyExpressionForResolvedArguments()) { |
||||
|
return Futures.transform(super.performCalculation(updatedArgs, ctx), telemetryCfResult -> |
||||
|
PropagationCalculatedFieldResult.builder() |
||||
|
.propagationEntityIds(propagationArgumentEntry.getPropagationEntityIds()) |
||||
|
.result((TelemetryCalculatedFieldResult) telemetryCfResult) |
||||
|
.build(), |
||||
|
MoreExecutors.directExecutor()); |
||||
|
} |
||||
|
return Futures.immediateFuture(PropagationCalculatedFieldResult.builder() |
||||
|
.propagationEntityIds(propagationArgumentEntry.getPropagationEntityIds()) |
||||
|
.result(toTelemetryResult(ctx)) |
||||
|
.build()); |
||||
|
} |
||||
|
|
||||
|
private TelemetryCalculatedFieldResult toTelemetryResult(CalculatedFieldCtx ctx) { |
||||
|
Output output = ctx.getOutput(); |
||||
|
TelemetryCalculatedFieldResult.TelemetryCalculatedFieldResultBuilder telemetryCfBuilder = |
||||
|
TelemetryCalculatedFieldResult.builder() |
||||
|
.type(output.getType()) |
||||
|
.scope(output.getScope()); |
||||
|
ObjectNode valuesNode = JacksonUtil.newObjectNode(); |
||||
|
arguments.forEach((outputKey, argumentEntry) -> { |
||||
|
if (argumentEntry instanceof PropagationArgumentEntry) { |
||||
|
return; |
||||
|
} |
||||
|
if (argumentEntry instanceof SingleValueArgumentEntry singleArgumentEntry) { |
||||
|
JacksonUtil.addKvEntry(valuesNode, singleArgumentEntry.getKvEntryValue(), outputKey); |
||||
|
return; |
||||
|
} |
||||
|
throw new IllegalArgumentException("Unsupported argument type: " + argumentEntry.getType() + " detected for argument: " + outputKey + ". " + |
||||
|
"Only Latest telemetry or Attribute arguments supported for 'Arguments Only' propagation mode!"); |
||||
|
}); |
||||
|
ObjectNode result = toSimpleResult(output.getType() == OutputType.TIME_SERIES, valuesNode); |
||||
|
telemetryCfBuilder.result(result); |
||||
|
return telemetryCfBuilder.build(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,143 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.cf.ctx.state; |
||||
|
|
||||
|
import org.junit.jupiter.api.BeforeEach; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.thingsboard.script.api.tbel.TbelCfArg; |
||||
|
import org.thingsboard.script.api.tbel.TbelCfPropagationArg; |
||||
|
import org.thingsboard.server.common.data.id.AssetId; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||
|
|
||||
|
public class PropagationArgumentEntryTest { |
||||
|
|
||||
|
private final AssetId ENTITY_1_ID = new AssetId(UUID.fromString("b0a8637d-6d67-43d5-a483-c0e391afe805")); |
||||
|
private final AssetId ENTITY_2_ID = new AssetId(UUID.fromString("7bd85073-ded5-414f-a2ef-bd56ad3dbf6a")); |
||||
|
private final AssetId ENTITY_3_ID = new AssetId(UUID.fromString("d64f3e51-2ec2-472f-b475-b095ef8bdc70")); |
||||
|
|
||||
|
private PropagationArgumentEntry entry; |
||||
|
|
||||
|
@BeforeEach |
||||
|
void setUp() { |
||||
|
List<EntityId> propagationEntityIds = new ArrayList<>(); |
||||
|
propagationEntityIds.add(ENTITY_1_ID); |
||||
|
propagationEntityIds.add(ENTITY_2_ID); |
||||
|
entry = new PropagationArgumentEntry(propagationEntityIds); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testArgumentEntryType() { |
||||
|
assertThat(entry.getType()).isEqualTo(ArgumentEntryType.PROPAGATION); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testIsEmpty() { |
||||
|
PropagationArgumentEntry emptyEntry = new PropagationArgumentEntry(List.of()); |
||||
|
assertThat(emptyEntry.isEmpty()).isTrue(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testIsEmptyWhenNullList() { |
||||
|
PropagationArgumentEntry nullListEntry = new PropagationArgumentEntry(null); |
||||
|
assertThat(nullListEntry.isEmpty()).isTrue(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testGetValueReturnsPropagationIds() { |
||||
|
assertThat(entry.getValue()).isInstanceOf(List.class); |
||||
|
@SuppressWarnings("unchecked") |
||||
|
List<AssetId> value = (List<AssetId>) entry.getValue(); |
||||
|
assertThat(value).containsExactly(ENTITY_1_ID, ENTITY_2_ID); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testUpdateEntryWhenSingleEntryPassed() { |
||||
|
assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry())) |
||||
|
.isInstanceOf(IllegalArgumentException.class) |
||||
|
.hasMessage("Unsupported argument entry type for propagation argument entry: SINGLE_VALUE"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testUpdateEntryWhenRollingEntryPassed() { |
||||
|
assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) |
||||
|
.isInstanceOf(IllegalArgumentException.class) |
||||
|
.hasMessage("Unsupported argument entry type for propagation argument entry: TS_ROLLING"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testUpdateEntryReplacesWithNewIds() { |
||||
|
var newIds = new ArrayList<EntityId>(List.of(ENTITY_3_ID, ENTITY_1_ID)); |
||||
|
var updated = new PropagationArgumentEntry(newIds); |
||||
|
|
||||
|
boolean changed = entry.updateEntry(updated); |
||||
|
|
||||
|
assertThat(changed).isTrue(); |
||||
|
assertThat(entry.getPropagationEntityIds()).containsExactlyElementsOf(newIds); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testUpdateEntryClearsWhenNewEntryIsEmpty() { |
||||
|
var updatedEmpty = new PropagationArgumentEntry(List.of()); |
||||
|
|
||||
|
boolean changed = entry.updateEntry(updatedEmpty); |
||||
|
|
||||
|
assertThat(changed).isTrue(); |
||||
|
assertThat(entry.getPropagationEntityIds()).isEmpty(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testUpdateEntryClearsWhenNewEntryIsNullList() { |
||||
|
var updatedNull = new PropagationArgumentEntry(null); |
||||
|
|
||||
|
boolean changed = entry.updateEntry(updatedNull); |
||||
|
|
||||
|
assertThat(changed).isTrue(); |
||||
|
assertThat(entry.getPropagationEntityIds()).isEmpty(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
@SuppressWarnings("unchecked") |
||||
|
void testToTbelCfArgWithValues() { |
||||
|
TbelCfArg arg = entry.toTbelCfArg(); |
||||
|
assertThat(arg).isInstanceOf(TbelCfPropagationArg.class); |
||||
|
|
||||
|
TbelCfPropagationArg tbelCfPropagationArg = (TbelCfPropagationArg) arg; |
||||
|
assertThat(tbelCfPropagationArg.getValue()).isInstanceOf(List.class); |
||||
|
assertThat((List<EntityId>) tbelCfPropagationArg.getValue()).containsExactly(ENTITY_1_ID, ENTITY_2_ID); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@Test |
||||
|
@SuppressWarnings("unchecked") |
||||
|
void testToTbelCfArgWithEmptyValues() { |
||||
|
var empty = new PropagationArgumentEntry(List.of()); |
||||
|
TbelCfArg emptyArg = empty.toTbelCfArg(); |
||||
|
assertThat(emptyArg).isInstanceOf(TbelCfPropagationArg.class); |
||||
|
|
||||
|
TbelCfPropagationArg tbelCfPropagationArg = (TbelCfPropagationArg) emptyArg; |
||||
|
assertThat(tbelCfPropagationArg.getValue()).isInstanceOf(List.class); |
||||
|
assertThat((List<EntityId>) tbelCfPropagationArg.getValue()).isEmpty(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,247 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.cf.ctx.state; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
||||
|
import io.micrometer.core.instrument.simple.SimpleMeterRegistry; |
||||
|
import org.junit.jupiter.api.BeforeEach; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.boot.test.context.SpringBootTest; |
||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; |
||||
|
import org.thingsboard.script.api.tbel.TbelInvokeService; |
||||
|
import org.thingsboard.server.actors.ActorSystemContext; |
||||
|
import org.thingsboard.server.common.data.AttributeScope; |
||||
|
import org.thingsboard.server.common.data.cf.CalculatedField; |
||||
|
import org.thingsboard.server.common.data.cf.CalculatedFieldType; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.Argument; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.ArgumentType; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.Output; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.OutputType; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; |
||||
|
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; |
||||
|
import org.thingsboard.server.common.data.id.AssetId; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.kv.DoubleDataEntry; |
||||
|
import org.thingsboard.server.common.data.relation.EntityRelation; |
||||
|
import org.thingsboard.server.common.data.relation.EntitySearchDirection; |
||||
|
import org.thingsboard.server.common.stats.DefaultStatsFactory; |
||||
|
import org.thingsboard.server.dao.usagerecord.ApiLimitService; |
||||
|
import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult; |
||||
|
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; |
||||
|
import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Collections; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.UUID; |
||||
|
import java.util.concurrent.ExecutionException; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.mockito.ArgumentMatchers.any; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; |
||||
|
|
||||
|
@SpringBootTest(classes = {SimpleMeterRegistry.class, DefaultStatsFactory.class, DefaultTbelInvokeService.class}) |
||||
|
public class PropagationCalculatedFieldStateTest { |
||||
|
|
||||
|
private static final String TEMPERATURE_ARGUMENT_NAME = "t"; |
||||
|
private static final String TEST_RESULT_EXPRESSION_KEY = "testResult"; |
||||
|
private static final double TEMPERATURE_VALUE = 12.5; |
||||
|
|
||||
|
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("6c3513cb-85e7-4510-8746-1ba01859a8ce")); |
||||
|
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("be960a50-c029-4698-b2ec-c56a543c561c")); |
||||
|
private final AssetId ASSET_ID_1 = new AssetId(UUID.fromString("d26f0e5b-7d7d-4a61-9f5e-08ab97b30734")); |
||||
|
private final AssetId ASSET_ID_2 = new AssetId(UUID.fromString("1933a317-4df5-4d36-9800-68aded74579b")); |
||||
|
|
||||
|
private final SingleValueArgumentEntry singleValueArgEntry = |
||||
|
new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", TEMPERATURE_VALUE), 99L); |
||||
|
|
||||
|
private final PropagationArgumentEntry propagationArgEntry = |
||||
|
new PropagationArgumentEntry(new ArrayList<>(List.of(ASSET_ID_2, ASSET_ID_1))); |
||||
|
|
||||
|
private PropagationCalculatedFieldState state; |
||||
|
private CalculatedFieldCtx ctx; |
||||
|
|
||||
|
@Autowired |
||||
|
private TbelInvokeService tbelInvokeService; |
||||
|
|
||||
|
@MockitoBean |
||||
|
private ApiLimitService apiLimitService; |
||||
|
|
||||
|
@MockitoBean |
||||
|
private ActorSystemContext actorSystemContext; |
||||
|
|
||||
|
@BeforeEach |
||||
|
void setUp() { |
||||
|
when(actorSystemContext.getTbelInvokeService()).thenReturn(tbelInvokeService); |
||||
|
when(actorSystemContext.getApiLimitService()).thenReturn(apiLimitService); |
||||
|
when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); |
||||
|
} |
||||
|
|
||||
|
void initCtxAndState(boolean applyExpressionToResolvedArguments) { |
||||
|
ctx = new CalculatedFieldCtx(getCalculatedField(applyExpressionToResolvedArguments), actorSystemContext); |
||||
|
ctx.init(); |
||||
|
|
||||
|
state = new PropagationCalculatedFieldState(ctx.getEntityId()); |
||||
|
state.setCtx(ctx, null); |
||||
|
state.init(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testType() { |
||||
|
initCtxAndState(false); |
||||
|
assertThat(state.getType()).isEqualTo(CalculatedFieldType.PROPAGATION); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testInitAddsRequiredArgument() { |
||||
|
initCtxAndState(false); |
||||
|
assertThat(state.getRequiredArguments()).containsExactlyInAnyOrder(TEMPERATURE_ARGUMENT_NAME); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testIsReadyReturnFalseWhenNoArgumentsSet() { |
||||
|
initCtxAndState(false); |
||||
|
assertThat(state.isReady()).isFalse(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testIsReadyWhenPropagationArgIsNull() { |
||||
|
initCtxAndState(false); |
||||
|
state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); |
||||
|
assertThat(state.isReady()).isFalse(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testIsReadyWhenPropagationArgIsEmpty() { |
||||
|
initCtxAndState(false); |
||||
|
state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); |
||||
|
state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList())); |
||||
|
assertThat(state.isReady()).isFalse(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testIsReadyWhenPropagationArgHasEntities() { |
||||
|
initCtxAndState(false); |
||||
|
state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); |
||||
|
state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); |
||||
|
assertThat(state.isReady()).isTrue(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@Test |
||||
|
void testPerformCalculationWithEmptyPropagationArg() throws Exception { |
||||
|
initCtxAndState(false); |
||||
|
state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList())); |
||||
|
|
||||
|
PropagationCalculatedFieldResult result = performCalculation(); |
||||
|
|
||||
|
assertThat(result).isNotNull(); |
||||
|
assertThat(result.isEmpty()).isTrue(); |
||||
|
assertThat(result.getPropagationEntityIds()).isNullOrEmpty(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testPerformCalculationWithArgumentsOnlyMode() throws Exception { |
||||
|
initCtxAndState(false); |
||||
|
state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); |
||||
|
state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); |
||||
|
|
||||
|
PropagationCalculatedFieldResult propagationResult = performCalculation(); |
||||
|
|
||||
|
assertThat(propagationResult).isNotNull(); |
||||
|
assertThat(propagationResult.isEmpty()).isFalse(); |
||||
|
assertThat(propagationResult.getPropagationEntityIds()).containsExactly(ASSET_ID_2, ASSET_ID_1); |
||||
|
|
||||
|
TelemetryCalculatedFieldResult result = propagationResult.getResult(); |
||||
|
assertThat(result).isNotNull(); |
||||
|
assertThat(result.getType()).isEqualTo(OutputType.ATTRIBUTES); |
||||
|
assertThat(result.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE); |
||||
|
|
||||
|
ObjectNode expectedNode = JacksonUtil.newObjectNode(); |
||||
|
JacksonUtil.addKvEntry(expectedNode, singleValueArgEntry.getKvEntryValue(), TEMPERATURE_ARGUMENT_NAME); |
||||
|
|
||||
|
assertThat(result.getResult()).isEqualTo(expectedNode); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void testPerformCalculationWithExpressionResultMode() throws Exception { |
||||
|
initCtxAndState(true); |
||||
|
state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); |
||||
|
state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); |
||||
|
|
||||
|
PropagationCalculatedFieldResult propagationResult = performCalculation(); |
||||
|
|
||||
|
assertThat(propagationResult).isNotNull(); |
||||
|
assertThat(propagationResult.isEmpty()).isFalse(); |
||||
|
assertThat(propagationResult.getPropagationEntityIds()).containsExactly(ASSET_ID_2, ASSET_ID_1); |
||||
|
|
||||
|
TelemetryCalculatedFieldResult result = propagationResult.getResult(); |
||||
|
assertThat(result).isNotNull(); |
||||
|
assertThat(result.getType()).isEqualTo(OutputType.ATTRIBUTES); |
||||
|
assertThat(result.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE); |
||||
|
|
||||
|
ObjectNode expectedNode = JacksonUtil.newObjectNode(); |
||||
|
expectedNode.put(TEST_RESULT_EXPRESSION_KEY, TEMPERATURE_VALUE * 2); |
||||
|
|
||||
|
assertThat(result.getResult()).isEqualTo(expectedNode); |
||||
|
} |
||||
|
|
||||
|
private CalculatedField getCalculatedField(boolean applyExpressionToResolvedArguments) { |
||||
|
CalculatedField calculatedField = new CalculatedField(); |
||||
|
calculatedField.setTenantId(TENANT_ID); |
||||
|
calculatedField.setEntityId(DEVICE_ID); |
||||
|
calculatedField.setType(CalculatedFieldType.PROPAGATION); |
||||
|
calculatedField.setName("Test Propagation CF"); |
||||
|
calculatedField.setConfigurationVersion(1); |
||||
|
calculatedField.setConfiguration(getCalculatedFieldConfig(applyExpressionToResolvedArguments)); |
||||
|
calculatedField.setVersion(1L); |
||||
|
return calculatedField; |
||||
|
} |
||||
|
|
||||
|
private CalculatedFieldConfiguration getCalculatedFieldConfig(boolean applyExpressionToResolvedArguments) { |
||||
|
var config = new PropagationCalculatedFieldConfiguration(); |
||||
|
|
||||
|
config.setDirection(EntitySearchDirection.TO); |
||||
|
config.setRelationType(EntityRelation.CONTAINS_TYPE); |
||||
|
config.setApplyExpressionToResolvedArguments(applyExpressionToResolvedArguments); |
||||
|
|
||||
|
Argument temperatureArg = new Argument(); |
||||
|
ReferencedEntityKey tempKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); |
||||
|
temperatureArg.setRefEntityKey(tempKey); |
||||
|
|
||||
|
config.setArguments(Map.of(TEMPERATURE_ARGUMENT_NAME, temperatureArg)); |
||||
|
config.setExpression("{" + TEST_RESULT_EXPRESSION_KEY + ": " + TEMPERATURE_ARGUMENT_NAME + " * 2}"); |
||||
|
|
||||
|
Output output = new Output(); |
||||
|
output.setType(OutputType.ATTRIBUTES); |
||||
|
output.setScope(AttributeScope.SERVER_SCOPE); |
||||
|
config.setOutput(output); |
||||
|
|
||||
|
return config; |
||||
|
} |
||||
|
|
||||
|
private PropagationCalculatedFieldResult performCalculation() throws ExecutionException, InterruptedException { |
||||
|
return (PropagationCalculatedFieldResult) state.performCalculation(Collections.emptyMap(), ctx).get(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.common.data.cf.configuration; |
||||
|
|
||||
|
import jakarta.validation.constraints.NotBlank; |
||||
|
import jakarta.validation.constraints.NotNull; |
||||
|
import lombok.Data; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
import org.thingsboard.server.common.data.StringUtils; |
||||
|
import org.thingsboard.server.common.data.cf.CalculatedFieldType; |
||||
|
import org.thingsboard.server.common.data.relation.EntitySearchDirection; |
||||
|
import org.thingsboard.server.common.data.relation.RelationPathLevel; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
@Data |
||||
|
@EqualsAndHashCode(callSuper = true) |
||||
|
public class PropagationCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration { |
||||
|
|
||||
|
public static final String PROPAGATION_CONFIG_ARGUMENT = "propagationCtx"; |
||||
|
|
||||
|
@NotNull |
||||
|
private EntitySearchDirection direction; |
||||
|
@NotBlank |
||||
|
private String relationType; |
||||
|
|
||||
|
private boolean applyExpressionToResolvedArguments; |
||||
|
|
||||
|
@Override |
||||
|
public CalculatedFieldType getType() { |
||||
|
return CalculatedFieldType.PROPAGATION; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void validate() { |
||||
|
baseCalculatedFieldRestriction(); |
||||
|
propagationRestriction(); |
||||
|
if (!applyExpressionToResolvedArguments) { |
||||
|
arguments.forEach((name, argument) -> { |
||||
|
if (!currentEntitySource(argument)) { |
||||
|
throw new IllegalArgumentException("Arguments in 'Arguments only' propagation mode support only the 'Current entity' source entity type!"); |
||||
|
} |
||||
|
if (argument.getRefEntityKey() == null) { |
||||
|
throw new IllegalArgumentException("Argument: '" + name + "' doesn't have reference entity key configured!"); |
||||
|
} |
||||
|
if (argument.getRefEntityKey().getType() == ArgumentType.TS_ROLLING) { |
||||
|
throw new IllegalArgumentException("Argument type: 'Time series rolling' detected for argument: '" + name + "'. " + |
||||
|
"Only 'Attribute' or 'Latest telemetry' arguments are allowed for 'Arguments only' propagation mode!"); |
||||
|
} |
||||
|
}); |
||||
|
} else { |
||||
|
boolean noneMatchCurrentEntitySource = arguments.entrySet() |
||||
|
.stream() |
||||
|
.noneMatch(entry -> currentEntitySource(entry.getValue())); |
||||
|
if (noneMatchCurrentEntitySource) { |
||||
|
throw new IllegalArgumentException("At least one argument must be configured with the 'Current entity' " + |
||||
|
"source entity type for 'Expression result' propagation mode!"); |
||||
|
} |
||||
|
if (StringUtils.isBlank(expression)) { |
||||
|
throw new IllegalArgumentException("Expression must be specified for 'Expression result' propagation mode!"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Argument toPropagationArgument() { |
||||
|
var refDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); |
||||
|
refDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(direction, relationType))); |
||||
|
var propagationArgument = new Argument(); |
||||
|
propagationArgument.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); |
||||
|
return propagationArgument; |
||||
|
} |
||||
|
|
||||
|
private void propagationRestriction() { |
||||
|
if (arguments.entrySet().stream().anyMatch(entry -> entry.getKey().equals(PROPAGATION_CONFIG_ARGUMENT))) { |
||||
|
throw new IllegalArgumentException("Argument name '" + PROPAGATION_CONFIG_ARGUMENT + "' is reserved and cannot be used."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private boolean currentEntitySource(Argument argument) { |
||||
|
return argument.getRefEntityId() == null && argument.getRefDynamicSourceConfiguration() == null; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,153 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.common.data.cf.configuration; |
||||
|
|
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.junit.jupiter.api.extension.ExtendWith; |
||||
|
import org.mockito.junit.jupiter.MockitoExtension; |
||||
|
import org.thingsboard.server.common.data.cf.CalculatedFieldType; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.relation.EntityRelation; |
||||
|
import org.thingsboard.server.common.data.relation.EntitySearchDirection; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||
|
import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; |
||||
|
|
||||
|
@ExtendWith(MockitoExtension.class) |
||||
|
public class PropagationCalculatedFieldConfigurationTest { |
||||
|
|
||||
|
@Test |
||||
|
void typeShouldBePropagation() { |
||||
|
var cfg = new PropagationCalculatedFieldConfiguration(); |
||||
|
assertThat(cfg.getType()).isEqualTo(CalculatedFieldType.PROPAGATION); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void validateShouldThrowWhenConfigurationDisallowArgumentsWithReferencedEntity() { |
||||
|
var cfg = new PropagationCalculatedFieldConfiguration(); |
||||
|
Argument argumentWithRefEntityIdSet = new Argument(); |
||||
|
argumentWithRefEntityIdSet.setRefEntityId(new DeviceId(UUID.fromString("bda14084-f40e-4acc-9b85-9d1dd209bb64"))); |
||||
|
cfg.setArguments(Map.of("argumentWithRefEntityIdSet", argumentWithRefEntityIdSet)); |
||||
|
assertThatThrownBy(cfg::validate) |
||||
|
.isInstanceOf(IllegalArgumentException.class) |
||||
|
.hasMessage("Arguments in 'Arguments only' propagation mode support only the 'Current entity' source entity type!"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void validateShouldThrowWhenConfigurationDisallowArgumentsWithDynamicReferenceConfiguration() { |
||||
|
var cfg = new PropagationCalculatedFieldConfiguration(); |
||||
|
Argument argumentWithDynamicRefEntitySource = new Argument(); |
||||
|
argumentWithDynamicRefEntitySource.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); |
||||
|
cfg.setArguments(Map.of("argumentWithDynamicRefEntitySource", argumentWithDynamicRefEntitySource)); |
||||
|
assertThatThrownBy(cfg::validate) |
||||
|
.isInstanceOf(IllegalArgumentException.class) |
||||
|
.hasMessage("Arguments in 'Arguments only' propagation mode support only the 'Current entity' source entity type!"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void validateShouldThrowWhenConfigurationHasNoArgumentsWithCurrentEntitySource() { |
||||
|
var cfg = new PropagationCalculatedFieldConfiguration(); |
||||
|
Argument argumentWithRefEntityIdSet = new Argument(); |
||||
|
argumentWithRefEntityIdSet.setRefEntityId(new DeviceId(UUID.fromString("3703e895-3f9b-4b75-a715-b68f1ad51944"))); |
||||
|
cfg.setArguments(Map.of("argumentWithRefEntityIdSet", argumentWithRefEntityIdSet)); |
||||
|
cfg.setApplyExpressionToResolvedArguments(true); |
||||
|
assertThatThrownBy(cfg::validate) |
||||
|
.isInstanceOf(IllegalArgumentException.class) |
||||
|
.hasMessage("At least one argument must be configured with the 'Current entity' " + |
||||
|
"source entity type for 'Expression result' propagation mode!"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void validateShouldThrowWhenUsedReservedPropagationArgumentName() { |
||||
|
var cfg = new PropagationCalculatedFieldConfiguration(); |
||||
|
cfg.setArguments(Map.of(PROPAGATION_CONFIG_ARGUMENT, new Argument())); |
||||
|
assertThatThrownBy(cfg::validate) |
||||
|
.isInstanceOf(IllegalArgumentException.class) |
||||
|
.hasMessage("Argument name '" + PROPAGATION_CONFIG_ARGUMENT + "' is reserved and cannot be used."); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void validateShouldThrowWhenUsedReservedCtxArgumentName() { |
||||
|
var cfg = new PropagationCalculatedFieldConfiguration(); |
||||
|
cfg.setArguments(Map.of("ctx", new Argument())); |
||||
|
assertThatThrownBy(cfg::validate) |
||||
|
.isInstanceOf(IllegalArgumentException.class) |
||||
|
.hasMessage("Argument name 'ctx' is reserved and cannot be used."); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void validateShouldThrowWhenReferencedEntityKeyIsNotSet() { |
||||
|
var cfg = new PropagationCalculatedFieldConfiguration(); |
||||
|
Argument argument = new Argument(); |
||||
|
cfg.setArguments(Map.of("someArgumentName", argument)); |
||||
|
assertThatThrownBy(cfg::validate) |
||||
|
.isInstanceOf(IllegalArgumentException.class) |
||||
|
.hasMessage("Argument: 'someArgumentName' doesn't have reference entity key configured!"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void validateShouldThrowWhenReferencedEntityKeyTypeIsTsRolling() { |
||||
|
var cfg = new PropagationCalculatedFieldConfiguration(); |
||||
|
ReferencedEntityKey referencedEntityKey = new ReferencedEntityKey("someKey", ArgumentType.TS_ROLLING, null); |
||||
|
Argument argument = new Argument(); |
||||
|
argument.setRefEntityKey(referencedEntityKey); |
||||
|
cfg.setArguments(Map.of("someArgumentName", argument)); |
||||
|
assertThatThrownBy(cfg::validate) |
||||
|
.isInstanceOf(IllegalArgumentException.class) |
||||
|
.hasMessage("Argument type: 'Time series rolling' detected for argument: 'someArgumentName'. " + |
||||
|
"Only 'Attribute' or 'Latest telemetry' arguments are allowed for 'Arguments only' propagation mode!"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void validateShouldThrowWhenExpressionIsNotSet() { |
||||
|
var cfg = new PropagationCalculatedFieldConfiguration(); |
||||
|
cfg.setArguments(Map.of("someArgumentName", new Argument())); |
||||
|
cfg.setApplyExpressionToResolvedArguments(true); |
||||
|
assertThatThrownBy(cfg::validate) |
||||
|
.isInstanceOf(IllegalArgumentException.class) |
||||
|
.hasMessage("Expression must be specified for 'Expression result' propagation mode!"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void validateToPropagationArgumentMethodCallReturnCorrectArgument() { |
||||
|
var cfg = new PropagationCalculatedFieldConfiguration(); |
||||
|
cfg.setDirection(EntitySearchDirection.TO); |
||||
|
cfg.setRelationType(EntityRelation.CONTAINS_TYPE); |
||||
|
|
||||
|
Argument propagationArgument = cfg.toPropagationArgument(); |
||||
|
assertThat(propagationArgument).isNotNull(); |
||||
|
assertThat(propagationArgument.getRefEntityId()).isNull(); |
||||
|
assertThat(propagationArgument.getRefEntityKey()).isNull(); |
||||
|
assertThat(propagationArgument.getDefaultValue()).isNull(); |
||||
|
assertThat(propagationArgument.getTimeWindow()).isNull(); |
||||
|
assertThat(propagationArgument.getLimit()).isNull(); |
||||
|
|
||||
|
assertThat(propagationArgument.getRefDynamicSourceConfiguration()) |
||||
|
.isNotNull() |
||||
|
.isInstanceOf(RelationPathQueryDynamicSourceConfiguration.class); |
||||
|
var refDynamicSourceConfiguration = (RelationPathQueryDynamicSourceConfiguration) propagationArgument.getRefDynamicSourceConfiguration(); |
||||
|
assertThat(refDynamicSourceConfiguration.getLevels()).isNotEmpty().hasSize(1); |
||||
|
|
||||
|
var relationPathLevel = refDynamicSourceConfiguration.getLevels().get(0); |
||||
|
assertThat(relationPathLevel.direction()).isEqualTo(EntitySearchDirection.TO); |
||||
|
assertThat(relationPathLevel.relationType()).isEqualTo(EntityRelation.CONTAINS_TYPE); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.script.api.tbel; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonCreator; |
||||
|
import com.fasterxml.jackson.annotation.JsonProperty; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
public class TbelCfPropagationArg implements TbelCfArg { |
||||
|
|
||||
|
private final Object value; |
||||
|
|
||||
|
@JsonCreator |
||||
|
public TbelCfPropagationArg(@JsonProperty("value") Object value) { |
||||
|
this.value = value; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getType() { |
||||
|
return "PROPAGATION_CF_ARGUMENT_VALUE"; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public long memorySize() { |
||||
|
return OBJ_SIZE; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { |
||||
|
CalculatedFieldDialogComponent |
||||
|
} from '@home/components/calculated-fields/components/dialog/calculated-field-dialog.component'; |
||||
|
import { |
||||
|
CalculatedFieldScriptTestDialogComponent |
||||
|
} from '@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component'; |
||||
|
import { |
||||
|
CalculatedFieldTestArgumentsComponent |
||||
|
} from '@home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component'; |
||||
|
import { |
||||
|
EntityDebugSettingsButtonComponent |
||||
|
} from '@home/components/entity/debug/entity-debug-settings-button.component'; |
||||
|
import { |
||||
|
GeofencingConfigurationModule |
||||
|
} from '@home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.module'; |
||||
|
import { |
||||
|
SimpleConfigurationModule |
||||
|
} from '@home/components/calculated-fields/components/simple-configuration/simple-configuration.module'; |
||||
|
import { |
||||
|
PropagationConfigurationModule |
||||
|
} from '@home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
declarations: [ |
||||
|
CalculatedFieldDialogComponent, |
||||
|
CalculatedFieldScriptTestDialogComponent, |
||||
|
CalculatedFieldTestArgumentsComponent, |
||||
|
], |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
GeofencingConfigurationModule, |
||||
|
EntityDebugSettingsButtonComponent, |
||||
|
SimpleConfigurationModule, |
||||
|
PropagationConfigurationModule, |
||||
|
], |
||||
|
exports: [ |
||||
|
CalculatedFieldDialogComponent, |
||||
|
CalculatedFieldScriptTestDialogComponent, |
||||
|
] |
||||
|
}) |
||||
|
export class CalculatedFieldsModule {} |
||||
@ -0,0 +1,45 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { |
||||
|
CalculatedFieldArgumentPanelComponent |
||||
|
} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component'; |
||||
|
import { |
||||
|
CalculatedFieldArgumentsTableComponent |
||||
|
} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component'; |
||||
|
import { |
||||
|
PropagateArgumentsTableComponent |
||||
|
} from '@home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
], |
||||
|
declarations: [ |
||||
|
CalculatedFieldArgumentPanelComponent, |
||||
|
CalculatedFieldArgumentsTableComponent, |
||||
|
PropagateArgumentsTableComponent |
||||
|
], |
||||
|
exports: [ |
||||
|
CalculatedFieldArgumentsTableComponent, |
||||
|
PropagateArgumentsTableComponent |
||||
|
] |
||||
|
}) |
||||
|
export class CalculatedFieldArgumentsTableModule {} |
||||
@ -0,0 +1,116 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
DestroyRef, |
||||
|
forwardRef, |
||||
|
OnInit, |
||||
|
Renderer2, |
||||
|
ViewContainerRef, |
||||
|
} from '@angular/core'; |
||||
|
import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, } from '@angular/forms'; |
||||
|
import { TbPopoverService } from '@shared/components/popover.service'; |
||||
|
import { EntityService } from '@core/http/entity.service'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { |
||||
|
CalculatedFieldArgumentsTableComponent |
||||
|
} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component'; |
||||
|
import { ArgumentEntityType, ArgumentType, CalculatedFieldArgumentValue } from '@shared/models/calculated-field.models'; |
||||
|
import { isDefined } from '@core/utils'; |
||||
|
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-propagate-arguments-table', |
||||
|
templateUrl: './calculated-field-arguments-table.component.html', |
||||
|
styleUrls: [`calculated-field-arguments-table.component.scss`], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => PropagateArgumentsTableComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => PropagateArgumentsTableComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
}) |
||||
|
export class PropagateArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent implements OnInit { |
||||
|
|
||||
|
constructor( |
||||
|
protected fb: FormBuilder, |
||||
|
protected popoverService: TbPopoverService, |
||||
|
protected viewContainerRef: ViewContainerRef, |
||||
|
protected cd: ChangeDetectorRef, |
||||
|
protected renderer: Renderer2, |
||||
|
protected entityService: EntityService, |
||||
|
protected destroyRef: DestroyRef, |
||||
|
protected store: Store<AppState> |
||||
|
) { |
||||
|
super(fb, popoverService, viewContainerRef, cd, renderer, entityService, destroyRef, store) |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.updatedValue(); |
||||
|
} |
||||
|
|
||||
|
protected changeIsScriptMode(): void { |
||||
|
this.updatedValue(); |
||||
|
super.changeIsScriptMode(); |
||||
|
} |
||||
|
|
||||
|
private updatedValue() { |
||||
|
if (this.isScript) { |
||||
|
this.argumentNameColumn = 'common.name'; |
||||
|
this.argumentNameColumnCopy = 'calculated-fields.copy-argument-name'; |
||||
|
this.displayColumns = ['name', 'entityType', 'target', 'type', 'key', 'actions']; |
||||
|
this.panelAdditionalCtx = null; |
||||
|
} else { |
||||
|
this.argumentNameColumn = 'calculated-fields.output-key'; |
||||
|
this.argumentNameColumnCopy = 'calculated-fields.copy-output-key'; |
||||
|
this.displayColumns = ['name', 'type', 'key', 'actions']; |
||||
|
this.panelAdditionalCtx = { |
||||
|
argumentEntityTypes: [ArgumentEntityType.Current], |
||||
|
isOutputKey: true |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected isEditButtonShowBadge(argument: CalculatedFieldArgumentValue): boolean { |
||||
|
if (!this.isScript && isDefined(argument?.refEntityId)) { |
||||
|
return false; |
||||
|
} |
||||
|
return super.isEditButtonShowBadge(argument); |
||||
|
} |
||||
|
|
||||
|
protected updateErrorText(): void { |
||||
|
if (!this.isScript && this.argumentsFormArray.controls.some(control => isDefined(control.value?.refEntityId))) { |
||||
|
this.errorText = 'calculated-fields.hint.arguments-propagate-argument-entity-type'; |
||||
|
} else if (!this.isScript && this.argumentsFormArray.controls.some(control => control.value.refEntityKey.type === ArgumentType.Rolling)) { |
||||
|
this.errorText = 'calculated-fields.hint.arguments-propagate-arguments-with-rolling'; |
||||
|
} else if (this.argumentsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) { |
||||
|
this.errorText = 'calculated-fields.hint.arguments-entity-not-found'; |
||||
|
} else if (!this.argumentsFormArray.controls.length) { |
||||
|
this.errorText = 'calculated-fields.hint.arguments-empty'; |
||||
|
} else { |
||||
|
this.errorText = ''; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
0
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.html → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html
0
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.html → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html
0
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.scss → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.scss
0
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.scss → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.scss
8
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts
8
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts
@ -0,0 +1,68 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2025 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div [formGroup]="geofencingConfiguration" class="tb-form-panel no-border no-padding"> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title tb-required" tb-hint-tooltip-icon="{{ 'calculated-fields.hint.entity-coordinates' | translate }}"> |
||||
|
{{ 'calculated-fields.entity-coordinates' | translate }} |
||||
|
</div> |
||||
|
<div class="flex items-start gap-3" formGroupName="entityCoordinates"> |
||||
|
<tb-entity-key-autocomplete class="flex-1" |
||||
|
placeholder="{{ 'calculated-fields.latitude-time-series-key' | translate }}" |
||||
|
requiredText="{{ 'calculated-fields.latitude-time-series-key-required' | translate }}" |
||||
|
formControlName="latitudeKeyName" |
||||
|
[dataKeyType]="DataKeyType.timeseries" |
||||
|
[entityFilter]="currentEntityFilter"/> |
||||
|
<tb-entity-key-autocomplete class="flex-1" |
||||
|
placeholder="{{ 'calculated-fields.longitude-time-series-key' | translate }}" |
||||
|
requiredText="{{ 'calculated-fields.longitude-time-series-key-required' | translate }}" |
||||
|
formControlName="longitudeKeyName" |
||||
|
[dataKeyType]="DataKeyType.timeseries" |
||||
|
[entityFilter]="currentEntityFilter"/> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title tb-required" tb-hint-tooltip-icon="{{ 'calculated-fields.hint.geofencing-zone-groups' | translate }}"> |
||||
|
{{ 'calculated-fields.geofencing-zone-groups' | translate }} |
||||
|
</div> |
||||
|
<tb-calculated-field-geofencing-zone-groups-table formControlName="zoneGroups" |
||||
|
[entityId]="entityId" |
||||
|
[tenantId]="tenantId" |
||||
|
[entityName]="entityName"/> |
||||
|
<div class="tb-form-row space-between flex-1 columns-xs" [class.!hidden]="!isRelatedEntity"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="scheduledUpdateEnabled"> |
||||
|
<div tb-hint-tooltip-icon="{{'calculated-fields.hint.zone-group-refresh-interval' | translate}}"> |
||||
|
{{ 'calculated-fields.zone-group-refresh-interval' | translate }} |
||||
|
</div> |
||||
|
</mat-slide-toggle> |
||||
|
<div class="flex flex-row items-center justify-start gap-2"> |
||||
|
<tb-time-unit-input required |
||||
|
inlineField |
||||
|
requiredText="{{ 'calculated-fields.hint.zone-group-refresh-interval-required' | translate }}" |
||||
|
minErrorText="{{ 'calculated-fields.hint.zone-group-refresh-interval-min' | translate: {min: minAllowedScheduledUpdateIntervalInSecForCF} }}" |
||||
|
[minTime]="minAllowedScheduledUpdateIntervalInSecForCF" |
||||
|
formControlName="scheduledUpdateInterval"> |
||||
|
</tb-time-unit-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<tb-calculate-field-output |
||||
|
formControlName="output" |
||||
|
[entityId]="entityId"> |
||||
|
</tb-calculate-field-output> |
||||
|
</div> |
||||
@ -0,0 +1,157 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormBuilder, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { |
||||
|
ArgumentEntityType, |
||||
|
CalculatedFieldGeofencing, |
||||
|
CalculatedFieldGeofencingConfiguration, |
||||
|
CalculatedFieldOutput, |
||||
|
CalculatedFieldType, |
||||
|
getCalculatedFieldCurrentEntityFilter, |
||||
|
OutputType |
||||
|
} from '@shared/models/calculated-field.models'; |
||||
|
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { getCurrentAuthState } from '@core/auth/auth.selectors'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
import { EntityFilter } from '@shared/models/query/query.models'; |
||||
|
import { EntityId } from '@shared/models/id/entity-id'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-geofencing-configuration', |
||||
|
templateUrl: './geofencing-configuration.component.html', |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => GeofencingConfigurationComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => GeofencingConfigurationComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
}) |
||||
|
export class GeofencingConfigurationComponent implements ControlValueAccessor, Validator, OnInit { |
||||
|
|
||||
|
@Input({required: true}) |
||||
|
entityId: EntityId; |
||||
|
|
||||
|
@Input({required: true}) |
||||
|
tenantId: string; |
||||
|
|
||||
|
@Input({required: true}) |
||||
|
entityName: string; |
||||
|
|
||||
|
readonly minAllowedScheduledUpdateIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedScheduledUpdateIntervalInSecForCF; |
||||
|
readonly DataKeyType = DataKeyType; |
||||
|
|
||||
|
geofencingConfiguration = this.fb.group({ |
||||
|
entityCoordinates: this.fb.group({ |
||||
|
latitudeKeyName: [null, [Validators.required]], |
||||
|
longitudeKeyName: [null, [Validators.required]], |
||||
|
}), |
||||
|
zoneGroups: this.fb.control<Record<string, CalculatedFieldGeofencing>>({}), |
||||
|
scheduledUpdateEnabled: [true], |
||||
|
scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF], |
||||
|
output: this.fb.control<CalculatedFieldOutput>({scope: AttributeScope.SERVER_SCOPE, type: OutputType.Timeseries}) |
||||
|
}); |
||||
|
|
||||
|
currentEntityFilter: EntityFilter; |
||||
|
isRelatedEntity: boolean; |
||||
|
|
||||
|
private propagateChange: (config: CalculatedFieldGeofencingConfiguration) => void = () => { }; |
||||
|
|
||||
|
constructor(private fb: FormBuilder, |
||||
|
private store: Store<AppState>) { |
||||
|
|
||||
|
this.geofencingConfiguration.get('zoneGroups').valueChanges |
||||
|
.pipe(takeUntilDestroyed()) |
||||
|
.subscribe((zoneGroups: Record<string, CalculatedFieldGeofencing>) => |
||||
|
this.checkRelatedEntity(zoneGroups) |
||||
|
); |
||||
|
|
||||
|
this.geofencingConfiguration.get('scheduledUpdateEnabled').valueChanges |
||||
|
.pipe(takeUntilDestroyed()) |
||||
|
.subscribe((value: boolean) => |
||||
|
this.checkScheduledUpdateEnabled(value) |
||||
|
); |
||||
|
|
||||
|
this.geofencingConfiguration.valueChanges.pipe( |
||||
|
takeUntilDestroyed() |
||||
|
).subscribe(() => { |
||||
|
this.updatedModel(this.geofencingConfiguration.getRawValue() as any); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId); |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.geofencingConfiguration.valid || this.geofencingConfiguration.status === "DISABLED" ? null : { geofencingConfigError: false }; |
||||
|
} |
||||
|
|
||||
|
writeValue(config: CalculatedFieldGeofencingConfiguration): void { |
||||
|
this.geofencingConfiguration.patchValue(config, {emitEvent: false}); |
||||
|
this.checkRelatedEntity(this.geofencingConfiguration.get('zoneGroups').value); |
||||
|
this.checkScheduledUpdateEnabled(this.geofencingConfiguration.get('scheduledUpdateEnabled').value); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: (config: CalculatedFieldGeofencingConfiguration) => void): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(_: any): void { } |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
if (isDisabled) { |
||||
|
this.geofencingConfiguration.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.geofencingConfiguration.enable({emitEvent: false}); |
||||
|
this.checkScheduledUpdateEnabled(this.geofencingConfiguration.get('scheduledUpdateEnabled').value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updatedModel(value: CalculatedFieldGeofencingConfiguration) { |
||||
|
value.type = CalculatedFieldType.GEOFENCING; |
||||
|
this.propagateChange(value) |
||||
|
} |
||||
|
|
||||
|
private checkScheduledUpdateEnabled(value: boolean) { |
||||
|
if (value) { |
||||
|
this.geofencingConfiguration.get('scheduledUpdateInterval').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.geofencingConfiguration.get('scheduledUpdateInterval').disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private checkRelatedEntity(zoneGroups: Record<string, CalculatedFieldGeofencing>) { |
||||
|
this.isRelatedEntity = Object.values(zoneGroups).some(zone => zone.refDynamicSourceConfiguration?.type === ArgumentEntityType.RelationQuery); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { |
||||
|
CalculatedFieldGeofencingZoneGroupsTableComponent |
||||
|
} from '@home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component'; |
||||
|
import { |
||||
|
CalculatedFieldGeofencingZoneGroupsPanelComponent |
||||
|
} from '@home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { |
||||
|
GeofencingConfigurationComponent |
||||
|
} from '@home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component'; |
||||
|
import { |
||||
|
CalculatedFieldOutputModule |
||||
|
} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
CalculatedFieldOutputModule |
||||
|
], |
||||
|
declarations: [ |
||||
|
CalculatedFieldGeofencingZoneGroupsTableComponent, |
||||
|
CalculatedFieldGeofencingZoneGroupsPanelComponent, |
||||
|
GeofencingConfigurationComponent |
||||
|
], |
||||
|
exports: [ |
||||
|
CalculatedFieldGeofencingZoneGroupsTableComponent, |
||||
|
CalculatedFieldGeofencingZoneGroupsPanelComponent, |
||||
|
GeofencingConfigurationComponent |
||||
|
] |
||||
|
}) |
||||
|
export class GeofencingConfigurationModule { |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,86 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2025 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-form-panel" [formGroup]="outputForm"> |
||||
|
<div class="tb-form-panel-title">{{ 'calculated-fields.output' | translate }}</div> |
||||
|
<div class="flex items-center gap-3"> |
||||
|
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-label>{{ 'calculated-fields.output-type' | translate }}</mat-label> |
||||
|
<mat-select formControlName="type"> |
||||
|
@for (type of outputTypes; track type) { |
||||
|
<mat-option [value]="type">{{ OutputTypeTranslations.get(type) | translate }}</mat-option> |
||||
|
} |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
@if (outputForm.get('type').value === OutputType.Attribute |
||||
|
&& (entityId.entityType === EntityType.DEVICE || entityId.entityType === EntityType.DEVICE_PROFILE)) { |
||||
|
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-label>{{ 'calculated-fields.attribute-scope' | translate }}</mat-label> |
||||
|
<mat-select formControlName="scope" class="w-full"> |
||||
|
<mat-option [value]="AttributeScope.SERVER_SCOPE"> |
||||
|
{{ 'calculated-fields.server-attributes' | translate }} |
||||
|
</mat-option> |
||||
|
<mat-option [value]="AttributeScope.SHARED_SCOPE"> |
||||
|
{{ 'calculated-fields.shared-attributes' | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
} |
||||
|
</div> |
||||
|
@if (simpleMode) { |
||||
|
<div class="flex items-start gap-3"> |
||||
|
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-label> |
||||
|
{{ |
||||
|
(outputForm.get('type').value === OutputType.Timeseries |
||||
|
? 'calculated-fields.timeseries-key' |
||||
|
: 'calculated-fields.attribute-key') |
||||
|
| translate |
||||
|
}} |
||||
|
</mat-label> |
||||
|
<input matInput formControlName="name" required> |
||||
|
@if (outputForm.get('name').errors && outputForm.get('name').touched) { |
||||
|
<mat-error> |
||||
|
@if (outputForm.get('name').hasError('required')) { |
||||
|
{{ 'common.hint.key-required' | translate }} |
||||
|
} @else if (outputForm.get('name').hasError('pattern')) { |
||||
|
{{ 'common.hint.key-pattern' | translate }} |
||||
|
} @else if (outputForm.get('name').hasError('maxlength')) { |
||||
|
{{ 'common.hint.key-max-length' | translate }} |
||||
|
} |
||||
|
</mat-error> |
||||
|
} |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-label>{{ 'calculated-fields.decimals-by-default' | translate }}</mat-label> |
||||
|
<input matInput type="number" formControlName="decimalsByDefault"> |
||||
|
@if (outputForm.get('decimalsByDefault').errors && outputForm.get('decimalsByDefault').touched) { |
||||
|
<mat-error>{{ 'calculated-fields.hint.decimals-range' | translate }}</mat-error> |
||||
|
} |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<ng-content select=".simpleMode"></ng-content> |
||||
|
<!-- <div class="tb-form-row" [formGroup]="configFormGroup"--> |
||||
|
<!-- *ngIf="outputFormGroup.get('type').value === OutputType.Timeseries">--> |
||||
|
<!-- <mat-slide-toggle class="mat-slide" formControlName="useLatestTs">--> |
||||
|
<!-- <div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.use-latest-timestamp' | translate }}" translate>--> |
||||
|
<!-- calculated-fields.use-latest-timestamp--> |
||||
|
<!-- </div>--> |
||||
|
<!-- </mat-slide-toggle>--> |
||||
|
<!-- </div>--> |
||||
|
} |
||||
|
</div> |
||||
@ -0,0 +1,148 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, DestroyRef, forwardRef, inject, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormBuilder, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { |
||||
|
CalculatedFieldOutput, |
||||
|
CalculatedFieldSimpleOutput, |
||||
|
OutputType, |
||||
|
OutputTypeTranslations |
||||
|
} from '@shared/models/calculated-field.models'; |
||||
|
import { digitsRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
import { EntityId } from '@shared/models/id/entity-id'; |
||||
|
import { EntityType } from '@shared/models/entity-type.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-calculate-field-output', |
||||
|
templateUrl: './calculated-field-output.component.html', |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => CalculatedFieldOutputComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => CalculatedFieldOutputComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
}) |
||||
|
export class CalculatedFieldOutputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { |
||||
|
|
||||
|
@Input() |
||||
|
simpleMode = false; |
||||
|
|
||||
|
@Input({required: true}) |
||||
|
entityId: EntityId; |
||||
|
|
||||
|
readonly outputTypes = Object.values(OutputType) as OutputType[]; |
||||
|
readonly OutputType = OutputType; |
||||
|
readonly AttributeScope = AttributeScope; |
||||
|
readonly OutputTypeTranslations = OutputTypeTranslations; |
||||
|
readonly EntityType = EntityType; |
||||
|
|
||||
|
private fb = inject(FormBuilder); |
||||
|
private destroyRef = inject(DestroyRef); |
||||
|
|
||||
|
outputForm = this.fb.group({ |
||||
|
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], |
||||
|
scope: [{value: AttributeScope.SERVER_SCOPE, disabled: true}], |
||||
|
type: [OutputType.Timeseries], |
||||
|
decimalsByDefault: [null as number, [Validators.min(0), Validators.max(15), Validators.pattern(digitsRegex)]], |
||||
|
}); |
||||
|
|
||||
|
private propagateChange: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void = () => { }; |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.outputForm.get('type').valueChanges |
||||
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
||||
|
.subscribe(type => this.toggleScopeByOutputType(type)); |
||||
|
|
||||
|
this.updatedFormWithMode(); |
||||
|
|
||||
|
this.outputForm.valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe((value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => { |
||||
|
this.updatedModel(value) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges): void { |
||||
|
for (const propName of Object.keys(changes)) { |
||||
|
const change = changes[propName]; |
||||
|
if (change.currentValue !== change.previousValue) { |
||||
|
if (propName === 'simpleMode') { |
||||
|
this.updatedFormWithMode(); |
||||
|
if (!change.firstChange) { |
||||
|
this.outputForm.updateValueAndValidity(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.outputForm.valid ? null : {outputConfig: false}; |
||||
|
} |
||||
|
|
||||
|
writeValue(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput): void { |
||||
|
this.outputForm.patchValue(value, {emitEvent: false}); |
||||
|
this.outputForm.get('type').updateValueAndValidity({onlySelf: true}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(_: any): void { } |
||||
|
|
||||
|
private updatedModel(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) { |
||||
|
if (this.simpleMode && 'name' in value) { |
||||
|
value.name = value.name?.trim() ?? ''; |
||||
|
} |
||||
|
this.propagateChange(value); |
||||
|
} |
||||
|
|
||||
|
private toggleScopeByOutputType(type: OutputType): void { |
||||
|
if (type === OutputType.Attribute) { |
||||
|
this.outputForm.get('scope').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.outputForm.get('scope').disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updatedFormWithMode(): void { |
||||
|
if (this.simpleMode) { |
||||
|
this.outputForm.get('name').enable({emitEvent: false}); |
||||
|
this.outputForm.get('decimalsByDefault').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.outputForm.get('name').disable({emitEvent: false}); |
||||
|
this.outputForm.get('decimalsByDefault').disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { |
||||
|
CalculatedFieldOutputComponent |
||||
|
} from '@home/components/calculated-fields/components/output/calculated-field-output.component'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
], |
||||
|
declarations: [ |
||||
|
CalculatedFieldOutputComponent |
||||
|
], |
||||
|
exports: [ |
||||
|
CalculatedFieldOutputComponent |
||||
|
] |
||||
|
}) |
||||
|
export class CalculatedFieldOutputModule { } |
||||
@ -0,0 +1,99 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2025 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div [formGroup]="propagateConfiguration" class="tb-form-panel no-border no-padding"> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" tbTruncateWithTooltip tb-hint-tooltip-icon="{{ 'calculated-fields.hint.propagation-path-related-entities' | translate }}"> |
||||
|
{{ 'calculated-fields.propagation-path-related-entities' | translate }} |
||||
|
</div> |
||||
|
<div class="flex gap-3 xs:flex-col"> |
||||
|
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic" hideRequiredMarker> |
||||
|
<mat-label>{{ 'calculated-fields.direction' | translate }}</mat-label> |
||||
|
<mat-select formControlName="direction"> |
||||
|
@for (direction of Directions; track direction) { |
||||
|
<mat-option [value]="direction">{{ PropagationDirectionTranslations.get(direction) | translate }}</mat-option> |
||||
|
} |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)" |
||||
|
class="flex-1" |
||||
|
panelWidth="" |
||||
|
additionalClass="" |
||||
|
required |
||||
|
[label]="'calculated-fields.relation-type' | translate" |
||||
|
[errorText]="'calculated-fields.hint.relation-type-required' | translate" |
||||
|
formControlName="relationType"> |
||||
|
</tb-string-autocomplete> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="flex flex-row items-center justify-between xs:flex-col xs:items-start xs:gap-3"> |
||||
|
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'calculated-fields.hint.data-propagate' | translate }}"> |
||||
|
{{ 'calculated-fields.data-propagate' | translate }} |
||||
|
</div> |
||||
|
<tb-toggle-select formControlName="applyExpressionToResolvedArguments"> |
||||
|
<tb-toggle-option [value]="false">{{ 'calculated-fields.propagate-type.arguments-only' | translate }}</tb-toggle-option> |
||||
|
<tb-toggle-option [value]="true">{{ 'calculated-fields.propagate-type.expression-result' | translate }}</tb-toggle-option> |
||||
|
</tb-toggle-select> |
||||
|
</div> |
||||
|
<tb-propagate-arguments-table formControlName="arguments" |
||||
|
[entityId]="entityId" |
||||
|
[tenantId]="tenantId" |
||||
|
[entityName]="entityName" |
||||
|
[isScript]="this.propagateConfiguration.get('applyExpressionToResolvedArguments').value"/> |
||||
|
</div> |
||||
|
<div class="tb-form-panel no-gap" [class.!hidden]="!this.propagateConfiguration.get('applyExpressionToResolvedArguments').value"> |
||||
|
<div class="tb-form-panel-title tb-required"> |
||||
|
{{ 'calculated-fields.expression' | translate }} |
||||
|
</div> |
||||
|
<div> |
||||
|
<tb-js-func required |
||||
|
formControlName="expression" |
||||
|
functionName="calculate" |
||||
|
[functionArgs]="functionArgs$ | async" |
||||
|
[disableUndefinedCheck]="true" |
||||
|
[scriptLanguage]="ScriptLanguage.TBEL" |
||||
|
[highlightRules]="argumentsHighlightRules$ | async" |
||||
|
[editorCompleter]="argumentsEditorCompleter$ | async" |
||||
|
[helpPopupStyle]="{ width: '1200px' }" |
||||
|
helpId="calculated-field/expression_fn"> |
||||
|
<div toolbarPrefixButton |
||||
|
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }} |
||||
|
</div> |
||||
|
<button toolbarSuffixButton |
||||
|
mat-icon-button |
||||
|
matTooltip="{{ 'calculated-fields.test-expression-function' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
class="tb-mat-32" |
||||
|
[disabled]="propagateConfiguration.get('arguments').invalid" |
||||
|
(click)="onTestScript()"> |
||||
|
<mat-icon class="material-icons" color="primary">bug_report</mat-icon> |
||||
|
</button> |
||||
|
</tb-js-func> |
||||
|
<div> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
type="button" |
||||
|
(click)="onTestScript()" |
||||
|
[disabled]="propagateConfiguration.get('arguments').invalid"> |
||||
|
{{ 'calculated-fields.test-expression-function' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<tb-calculate-field-output formControlName="output" [entityId]="entityId"> |
||||
|
</tb-calculate-field-output> |
||||
|
</div> |
||||
@ -0,0 +1,174 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, forwardRef, Input } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormBuilder, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { EntityId } from '@shared/models/id/entity-id'; |
||||
|
import { Observable, of } from 'rxjs'; |
||||
|
import { |
||||
|
calculatedFieldDefaultScript, |
||||
|
CalculatedFieldOutput, |
||||
|
CalculatedFieldPropagationConfiguration, |
||||
|
CalculatedFieldType, |
||||
|
getCalculatedFieldArgumentsEditorCompleter, |
||||
|
getCalculatedFieldArgumentsHighlights, |
||||
|
OutputType, |
||||
|
PropagationDirectionTranslations, |
||||
|
PropagationWithExpression |
||||
|
} from '@shared/models/calculated-field.models'; |
||||
|
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { map } from 'rxjs/operators'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
import { ScriptLanguage } from '@app/shared/models/rule-node.models'; |
||||
|
import { EntitySearchDirection } from '@shared/models/relation.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-propagation-configuration', |
||||
|
templateUrl: './propagation-configuration.component.html', |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => PropagationConfigurationComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => PropagationConfigurationComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
}) |
||||
|
export class PropagationConfigurationComponent implements ControlValueAccessor, Validator { |
||||
|
|
||||
|
@Input({required: true}) |
||||
|
entityId: EntityId; |
||||
|
|
||||
|
@Input({required: true}) |
||||
|
tenantId: string; |
||||
|
|
||||
|
@Input({required: true}) |
||||
|
entityName: string; |
||||
|
|
||||
|
@Input({required: true}) |
||||
|
testScript: () => Observable<string>; |
||||
|
|
||||
|
propagateConfiguration = this.fb.group({ |
||||
|
arguments: this.fb.control({}), |
||||
|
applyExpressionToResolvedArguments: [false], |
||||
|
direction: [EntitySearchDirection.TO, Validators.required], |
||||
|
relationType: ['Contains', Validators.required], |
||||
|
expression: [calculatedFieldDefaultScript], |
||||
|
output: this.fb.control<CalculatedFieldOutput>({ |
||||
|
scope: AttributeScope.SERVER_SCOPE, |
||||
|
type: OutputType.Timeseries, |
||||
|
}), |
||||
|
}); |
||||
|
|
||||
|
readonly ScriptLanguage = ScriptLanguage; |
||||
|
readonly CalculatedFieldType = CalculatedFieldType; |
||||
|
readonly OutputType = OutputType; |
||||
|
readonly Directions = Object.values(EntitySearchDirection) as Array<EntitySearchDirection>; |
||||
|
readonly PropagationDirectionTranslations = PropagationDirectionTranslations; |
||||
|
|
||||
|
functionArgs$ = this.propagateConfiguration.get('arguments').valueChanges.pipe( |
||||
|
map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)]) |
||||
|
); |
||||
|
|
||||
|
argumentsEditorCompleter$ = this.propagateConfiguration.get('arguments').valueChanges.pipe( |
||||
|
map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {})) |
||||
|
); |
||||
|
|
||||
|
argumentsHighlightRules$ = this.propagateConfiguration.get('arguments').valueChanges.pipe( |
||||
|
map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) |
||||
|
); |
||||
|
|
||||
|
private propagateChange: (config: CalculatedFieldPropagationConfiguration) => void = () => { }; |
||||
|
|
||||
|
constructor(private fb: FormBuilder) { |
||||
|
this.propagateConfiguration.get('applyExpressionToResolvedArguments').valueChanges.pipe( |
||||
|
takeUntilDestroyed() |
||||
|
).subscribe(() => { |
||||
|
this.updatedFormWithScript(); |
||||
|
}) |
||||
|
|
||||
|
this.propagateConfiguration.valueChanges.pipe( |
||||
|
takeUntilDestroyed() |
||||
|
).subscribe((value: CalculatedFieldPropagationConfiguration) => { |
||||
|
this.updatedModel(value); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.propagateConfiguration.valid || this.propagateConfiguration.status === "DISABLED" ? null : {invalidPropagateConfig: false}; |
||||
|
} |
||||
|
|
||||
|
writeValue(value: PropagationWithExpression): void { |
||||
|
value.expression = value.expression ?? calculatedFieldDefaultScript; |
||||
|
this.propagateConfiguration.patchValue(value, {emitEvent: false}); |
||||
|
this.updatedFormWithScript(); |
||||
|
setTimeout(() => { |
||||
|
this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: (config: CalculatedFieldPropagationConfiguration) => void): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(_: any): void { } |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
if (isDisabled) { |
||||
|
this.propagateConfiguration.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.propagateConfiguration.enable({emitEvent: false}); |
||||
|
this.updatedFormWithScript(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onTestScript() { |
||||
|
this.testScript().subscribe((expression) => { |
||||
|
this.propagateConfiguration.get('expression').setValue(expression); |
||||
|
this.propagateConfiguration.get('expression').markAsDirty(); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
fetchOptions(searchText: string): Observable<Array<string>> { |
||||
|
const search = searchText ? searchText?.toLowerCase() : ''; |
||||
|
return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search)))); |
||||
|
} |
||||
|
|
||||
|
private updatedModel(value: CalculatedFieldPropagationConfiguration): void { |
||||
|
value.type = CalculatedFieldType.PROPAGATION; |
||||
|
this.propagateChange(value); |
||||
|
} |
||||
|
|
||||
|
private updatedFormWithScript() { |
||||
|
if (this.propagateConfiguration.get('applyExpressionToResolvedArguments').value) { |
||||
|
this.propagateConfiguration.get('expression').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.propagateConfiguration.get('expression').disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { |
||||
|
CalculatedFieldOutputModule |
||||
|
} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; |
||||
|
import { |
||||
|
CalculatedFieldArgumentsTableModule |
||||
|
} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; |
||||
|
import { |
||||
|
PropagationConfigurationComponent |
||||
|
} from '@home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
CalculatedFieldOutputModule, |
||||
|
CalculatedFieldArgumentsTableModule, |
||||
|
], |
||||
|
declarations: [ |
||||
|
PropagationConfigurationComponent, |
||||
|
], |
||||
|
exports: [ |
||||
|
PropagationConfigurationComponent, |
||||
|
] |
||||
|
}) |
||||
|
export class PropagationConfigurationModule { } |
||||
@ -0,0 +1,98 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2025 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div [formGroup]="simpleConfiguration" class="tb-form-panel no-border no-padding"> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title tb-required">{{ 'calculated-fields.arguments' | translate }}</div> |
||||
|
<tb-calculated-field-arguments-table formControlName="arguments" |
||||
|
[entityId]="entityId" |
||||
|
[tenantId]="tenantId" |
||||
|
[entityName]="entityName" |
||||
|
[isScript]="isScript" /> |
||||
|
</div> |
||||
|
<div class="tb-form-panel no-gap"> |
||||
|
<div class="tb-form-panel-title tb-required"> |
||||
|
{{ (isScript ? 'calculated-fields.type.script' : 'calculated-fields.expression') | translate }} |
||||
|
</div> |
||||
|
<mat-form-field class="mt-3" appearance="outline" subscriptSizing="dynamic" [class.hidden]="isScript"> |
||||
|
<input matInput formControlName="expressionSIMPLE" maxlength="255" [placeholder]="'(temperature - 32) / 1.8'" |
||||
|
required> |
||||
|
<div matSuffix |
||||
|
class="pr-2" |
||||
|
[tb-help-popup]="'math/math-methods_fn'" |
||||
|
tb-help-popup-placement="left" |
||||
|
[tb-help-popup-style]="{maxWidth: '970px'}"> |
||||
|
</div> |
||||
|
@if (simpleConfiguration.get('expressionSIMPLE').errors && simpleConfiguration.get('expressionSIMPLE').touched) { |
||||
|
<mat-error> |
||||
|
@if (simpleConfiguration.get('expressionSIMPLE').hasError('required')) { |
||||
|
{{ 'calculated-fields.hint.expression-required' | translate }} |
||||
|
} @else if (simpleConfiguration.get('expressionSIMPLE').hasError('pattern')) { |
||||
|
{{ 'calculated-fields.hint.expression-invalid' | translate }} |
||||
|
} @else if (simpleConfiguration.get('expressionSIMPLE').hasError('maxLength')) { |
||||
|
{{ 'calculated-fields.hint.expression-max-length' | translate }} |
||||
|
} |
||||
|
</mat-error> |
||||
|
} @else { |
||||
|
<mat-hint>{{ 'calculated-fields.hint.expression' | translate }}</mat-hint> |
||||
|
} |
||||
|
</mat-form-field> |
||||
|
<div [class.hidden]="!isScript"> |
||||
|
<tb-js-func required |
||||
|
formControlName="expressionSCRIPT" |
||||
|
functionName="calculate" |
||||
|
[functionArgs]="functionArgs$ | async" |
||||
|
[disableUndefinedCheck]="true" |
||||
|
[scriptLanguage]="ScriptLanguage.TBEL" |
||||
|
[highlightRules]="argumentsHighlightRules$ | async" |
||||
|
[editorCompleter]="argumentsEditorCompleter$ | async" |
||||
|
[helpPopupStyle]="{ width: '1200px' }" |
||||
|
helpId="calculated-field/expression_fn"> |
||||
|
<div toolbarPrefixButton |
||||
|
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }} |
||||
|
</div> |
||||
|
<button toolbarSuffixButton |
||||
|
mat-icon-button |
||||
|
matTooltip="{{ 'calculated-fields.test-script-function' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
class="tb-mat-32" |
||||
|
[disabled]="simpleConfiguration.get('arguments').invalid" |
||||
|
(click)="onTestScript()"> |
||||
|
<mat-icon class="material-icons" color="primary">bug_report</mat-icon> |
||||
|
</button> |
||||
|
</tb-js-func> |
||||
|
<div> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
type="button" |
||||
|
(click)="onTestScript()" |
||||
|
[disabled]="simpleConfiguration.get('arguments').invalid"> |
||||
|
{{ 'calculated-fields.test-script-function' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<tb-calculate-field-output formControlName="output" [simpleMode]="!isScript" [entityId]="entityId"> |
||||
|
<div class="tb-form-row simpleMode" |
||||
|
[class.!hidden]="simpleConfiguration.get('output').value.type !== OutputType.Timeseries"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="useLatestTs"> |
||||
|
<div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.use-latest-timestamp' | translate }}" translate> |
||||
|
calculated-fields.use-latest-timestamp |
||||
|
</div> |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
</tb-calculate-field-output> |
||||
|
</div> |
||||
@ -0,0 +1,206 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormBuilder, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; |
||||
|
import { |
||||
|
calculatedFieldDefaultScript, |
||||
|
CalculatedFieldScriptConfiguration, |
||||
|
CalculatedFieldSimpleConfiguration, |
||||
|
CalculatedFieldSimpleOutput, |
||||
|
CalculatedFieldType, |
||||
|
getCalculatedFieldArgumentsEditorCompleter, |
||||
|
getCalculatedFieldArgumentsHighlights, |
||||
|
OutputType |
||||
|
} from '@shared/models/calculated-field.models'; |
||||
|
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
import { deepClone } from '@core/utils'; |
||||
|
import { EntityId } from '@shared/models/id/entity-id'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { ScriptLanguage } from '@shared/models/rule-node.models'; |
||||
|
import { map } from 'rxjs/operators'; |
||||
|
|
||||
|
type SimpeConfiguration = CalculatedFieldSimpleConfiguration | CalculatedFieldScriptConfiguration; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-simple-configuration', |
||||
|
templateUrl: './simple-configuration.component.html', |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => SimpleConfigurationComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => SimpleConfigurationComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
}) |
||||
|
export class SimpleConfigurationComponent implements ControlValueAccessor, Validator, OnChanges { |
||||
|
|
||||
|
@Input() |
||||
|
isScript: boolean; |
||||
|
|
||||
|
@Input({required: true}) |
||||
|
entityId: EntityId; |
||||
|
|
||||
|
@Input({required: true}) |
||||
|
tenantId: string; |
||||
|
|
||||
|
@Input({required: true}) |
||||
|
entityName: string; |
||||
|
|
||||
|
@Input({required: true}) |
||||
|
testScript: () => Observable<string>; |
||||
|
|
||||
|
simpleConfiguration = this.fb.group({ |
||||
|
arguments: this.fb.control({}), |
||||
|
expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], |
||||
|
expressionSCRIPT: [calculatedFieldDefaultScript], |
||||
|
output: this.fb.control<CalculatedFieldSimpleOutput>({ |
||||
|
name: '', |
||||
|
scope: AttributeScope.SERVER_SCOPE, |
||||
|
type: OutputType.Timeseries, |
||||
|
decimalsByDefault: null |
||||
|
}), |
||||
|
useLatestTs: [false] |
||||
|
}); |
||||
|
|
||||
|
readonly ScriptLanguage = ScriptLanguage; |
||||
|
readonly OutputType = OutputType; |
||||
|
|
||||
|
functionArgs$ = this.simpleConfiguration.get('arguments').valueChanges.pipe( |
||||
|
map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)]) |
||||
|
); |
||||
|
|
||||
|
argumentsEditorCompleter$ = this.simpleConfiguration.get('arguments').valueChanges.pipe( |
||||
|
map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {})) |
||||
|
); |
||||
|
|
||||
|
argumentsHighlightRules$ = this.simpleConfiguration.get('arguments').valueChanges.pipe( |
||||
|
map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) |
||||
|
); |
||||
|
|
||||
|
private propagateChange: (config: SimpeConfiguration) => void = () => { }; |
||||
|
|
||||
|
constructor(private fb: FormBuilder) { |
||||
|
this.simpleConfiguration.get('output').valueChanges.pipe( |
||||
|
takeUntilDestroyed(), |
||||
|
).subscribe(() => { |
||||
|
this.toggleScopeByOutputType(); |
||||
|
}); |
||||
|
|
||||
|
this.simpleConfiguration.valueChanges.pipe( |
||||
|
takeUntilDestroyed() |
||||
|
).subscribe((value) => { |
||||
|
const { expressionSIMPLE, expressionSCRIPT, ...config } = value; |
||||
|
const cfConfig = config as SimpeConfiguration; |
||||
|
cfConfig.expression = this.isScript ? expressionSCRIPT : expressionSIMPLE; |
||||
|
this.updatedModel(cfConfig); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges): void { |
||||
|
for (const propName of Object.keys(changes)) { |
||||
|
const change = changes[propName]; |
||||
|
if (change.currentValue !== change.previousValue) { |
||||
|
if (propName === 'isScript') { |
||||
|
this.updatedFormWithScript(); |
||||
|
if (!change.firstChange) { |
||||
|
this.simpleConfiguration.updateValueAndValidity(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.simpleConfiguration.valid || this.simpleConfiguration.status === "DISABLED" ? null : {invalidSimpleConfig: false}; |
||||
|
} |
||||
|
|
||||
|
writeValue(value: SimpeConfiguration): void { |
||||
|
const formValue: any = deepClone(value); |
||||
|
if (this.isScript) { |
||||
|
formValue.expressionSCRIPT = formValue.expression ?? calculatedFieldDefaultScript; |
||||
|
} else { |
||||
|
formValue.expressionSIMPLE = formValue.expression; |
||||
|
} |
||||
|
this.simpleConfiguration.patchValue(formValue, {emitEvent: false}); |
||||
|
this.updatedFormWithScript(); |
||||
|
setTimeout(() => { |
||||
|
this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: (config: SimpeConfiguration) => void): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(_: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
if (isDisabled) { |
||||
|
this.simpleConfiguration.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.simpleConfiguration.enable({emitEvent: false}); |
||||
|
this.updatedFormWithScript(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onTestScript() { |
||||
|
this.testScript().subscribe((expression) => { |
||||
|
this.simpleConfiguration.get('expressionSCRIPT').setValue(expression); |
||||
|
this.simpleConfiguration.get('expressionSCRIPT').markAsDirty(); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
private updatedModel(value: SimpeConfiguration): void { |
||||
|
value.type = this.isScript ? CalculatedFieldType.SCRIPT : CalculatedFieldType.SIMPLE; |
||||
|
this.propagateChange(value); |
||||
|
} |
||||
|
|
||||
|
private updatedFormWithScript() { |
||||
|
if (this.isScript) { |
||||
|
this.simpleConfiguration.get('expressionSIMPLE').disable({emitEvent: false}); |
||||
|
this.simpleConfiguration.get('expressionSCRIPT').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.simpleConfiguration.get('expressionSIMPLE').enable({emitEvent: false}); |
||||
|
this.simpleConfiguration.get('expressionSCRIPT').disable({emitEvent: false}); |
||||
|
} |
||||
|
this.toggleScopeByOutputType(); |
||||
|
} |
||||
|
|
||||
|
private toggleScopeByOutputType(): void { |
||||
|
if (this.isScript || this.simpleConfiguration.get('output').value.type === OutputType.Attribute) { |
||||
|
this.simpleConfiguration.get('useLatestTs').disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.simpleConfiguration.get('useLatestTs').enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { |
||||
|
SimpleConfigurationComponent |
||||
|
} from '@home/components/calculated-fields/components/simple-configuration/simple-configuration.component'; |
||||
|
import { |
||||
|
CalculatedFieldOutputModule |
||||
|
} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; |
||||
|
import { |
||||
|
CalculatedFieldArgumentsTableModule |
||||
|
} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
CalculatedFieldOutputModule, |
||||
|
CalculatedFieldArgumentsTableModule, |
||||
|
], |
||||
|
declarations: [ |
||||
|
SimpleConfigurationComponent, |
||||
|
], |
||||
|
exports: [ |
||||
|
SimpleConfigurationComponent |
||||
|
] |
||||
|
}) |
||||
|
export class SimpleConfigurationModule {} |
||||
Loading…
Reference in new issue