Browse Source

Merge pull request #14713 from thingsboard/rc

RC
pull/14521/merge
Viacheslav Klimov 9 months ago
committed by GitHub
parent
commit
7fece503fa
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 51
      .github/release.yml
  2. 15
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  3. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java
  4. 8
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  5. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java
  6. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java
  7. 5
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java
  8. 68
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationArgumentEntry.java
  9. 9
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java
  10. 9
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java
  11. 8
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java
  12. 3
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java
  13. 3
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java
  14. 44
      application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java
  15. 3
      application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java
  16. 2
      application/src/main/resources/thingsboard.yml
  17. 53
      application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java
  18. 2
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java
  19. 5
      application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java
  20. 25
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java
  21. 33
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java
  22. 15
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java
  23. 23
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java
  24. 17
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java
  25. 25
      application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java
  26. 21
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java
  27. 14
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java
  28. 69
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java
  29. 2
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java
  30. 86
      msa/pom.xml
  31. 112
      ui-ngx/src/app/core/services/calculated-field-form.service.ts
  32. 4
      ui-ngx/src/app/core/services/utils.service.ts
  33. 34
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  34. 63
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  35. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts
  36. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts
  37. 86
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  38. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts
  39. 159
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html
  40. 175
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts
  41. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html
  42. 16
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts
  43. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html
  44. 11
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts
  45. 19
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts
  46. 1
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts
  47. 2
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts
  48. 136
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html
  49. 52
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss
  50. 145
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts
  51. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts
  52. 112
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  53. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts
  54. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts
  55. 8
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts
  56. 32
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  57. 85
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  58. 10
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts
  59. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html
  60. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts
  61. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts
  62. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html
  63. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts
  64. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts
  65. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts
  66. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts
  67. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts
  68. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts
  69. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  70. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts
  71. 1
      ui-ngx/src/app/modules/home/components/device/device-info-filter.component.ts
  72. 4
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts
  73. 10
      ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts
  74. 16
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  75. 24
      ui-ngx/src/app/modules/home/components/event/event-table.component.ts
  76. 6
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.ts
  77. 2
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts
  78. 7
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts
  79. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html
  80. 7
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts
  81. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts
  82. 4
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
  83. 1
      ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html
  84. 6
      ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts
  85. 9
      ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts
  86. 2
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  87. 87
      ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts
  88. 30
      ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.html
  89. 49
      ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts
  90. 5
      ui-ngx/src/app/modules/home/pages/alarm/alarm.module.ts
  91. 10
      ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-field-page.module.ts
  92. 84
      ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-routing.module.ts
  93. 30
      ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.html
  94. 57
      ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.ts
  95. 4
      ui-ngx/src/app/modules/home/pages/home-pages.module.ts
  96. 8
      ui-ngx/src/app/modules/login/pages/login/create-password.component.html
  97. 15
      ui-ngx/src/app/modules/login/pages/login/create-password.component.ts
  98. 6
      ui-ngx/src/app/modules/login/pages/login/login.component.html
  99. 11
      ui-ngx/src/app/modules/login/pages/login/login.component.ts
  100. 8
      ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts

51
.github/release.yml

@ -23,27 +23,19 @@ changelog:
labels: labels:
- 'Major Core' - 'Major Core'
- 'Major Rule Engine' - 'Major Rule Engine'
exclude:
labels:
- 'Bug'
- title: 'Major UI' - title: 'Major UI'
labels: labels:
- 'Major UI' - 'Major UI'
exclude:
labels:
- 'Bug'
- title: 'Major Transport' - title: 'Major Transport'
labels: labels:
- 'Major Transport' - 'Major Transport'
exclude:
labels:
- 'Bug'
- title: 'Major Edge' - title: 'Major Edge'
labels: labels:
- 'Major Edge' - 'Major Edge'
exclude:
labels:
- 'Bug'
- title: 'Core & Rule Engine' - title: 'Core & Rule Engine'
labels: labels:
- 'Core' - 'Core'
@ -51,38 +43,63 @@ changelog:
exclude: exclude:
labels: labels:
- 'Bug' - 'Bug'
- title: 'UI' - title: 'UI'
labels: labels:
- 'UI' - 'UI'
exclude: exclude:
labels: labels:
- 'Bug' - 'Bug'
- title: 'Transport' - title: 'Transport'
labels: labels:
- 'Transport' - 'Transport'
exclude: exclude:
labels: labels:
- 'Bug' - 'Bug'
- title: 'Edge' - title: 'Edge'
labels: labels:
- 'Edge' - 'Edge'
exclude: exclude:
labels: labels:
- 'Bug' - 'Bug'
- title: 'Bug: Core & Rule Engine' - title: 'Bug: Core & Rule Engine'
labels: labels:
- 'Core'
- 'Rule Engine'
- 'Bug' - 'Bug'
exclude:
labels:
- 'UI'
- 'Transport'
- 'Edge'
- title: 'Bug: UI' - title: 'Bug: UI'
labels: labels:
- 'UI'
- 'Bug' - 'Bug'
exclude:
labels:
- 'Core'
- 'Rule Engine'
- 'Transport'
- 'Edge'
- title: 'Bug: Transport' - title: 'Bug: Transport'
labels: labels:
- 'Transport'
- 'Bug' - 'Bug'
exclude:
labels:
- 'Core'
- 'Rule Engine'
- 'UI'
- 'Edge'
- title: 'Bug: Edge' - title: 'Bug: Edge'
labels: labels:
- 'Edge'
- 'Bug' - 'Bug'
exclude:
labels:
- 'Core'
- 'Rule Engine'
- 'UI'
- 'Transport'

15
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java

@ -572,24 +572,24 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} }
private Map<String, ArgumentEntry> mapToArguments(CalculatedFieldCtx ctx, List<TsKvProto> data) { private Map<String, ArgumentEntry> mapToArguments(CalculatedFieldCtx ctx, List<TsKvProto> data) {
return mapToArguments(entityId, ctx.getMainEntityArguments(), Collections.emptyMap(), data); return mapToArguments(entityId, ctx, ctx.getMainEntityArguments(), Collections.emptyMap(), data);
} }
private Map<String, ArgumentEntry> mapToArguments(CalculatedFieldCtx ctx, EntityId entityId, List<TsKvProto> data) { private Map<String, ArgumentEntry> mapToArguments(CalculatedFieldCtx ctx, EntityId entityId, List<TsKvProto> data) {
return mapToArguments(entityId, ctx.getLinkedAndDynamicArgs(entityId), ctx.getRelatedEntityArguments(), data); return mapToArguments(entityId, ctx, ctx.getLinkedAndDynamicArgs(entityId), ctx.getRelatedEntityArguments(), data);
} }
private Map<String, ArgumentEntry> mapToArguments(EntityId originator, Map<ReferencedEntityKey, Set<String>> args, Map<ReferencedEntityKey, Set<String>> relatedEntityArgs, List<TsKvProto> data) { private Map<String, ArgumentEntry> mapToArguments(EntityId originator, CalculatedFieldCtx ctx, Map<ReferencedEntityKey, Set<String>> args, Map<ReferencedEntityKey, Set<String>> relatedEntityArgs, List<TsKvProto> data) {
Map<String, ArgumentEntry> arguments = new HashMap<>(); Map<String, ArgumentEntry> arguments = new HashMap<>();
if (!relatedEntityArgs.isEmpty() || !args.isEmpty()) { if (!relatedEntityArgs.isEmpty() || !args.isEmpty()) {
for (TsKvProto item : data) { for (TsKvProto item : data) {
ReferencedEntityKey key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_LATEST, null); ReferencedEntityKey key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_LATEST, null);
SingleValueArgumentEntry relatedArgIncoming = new SingleValueArgumentEntry(originator, item); SingleValueArgumentEntry relatedArgIncoming = new SingleValueArgumentEntry(originator, item);
mapLatest(relatedArgIncoming, relatedEntityArgs.get(key), arguments); mapLatest(ctx, relatedArgIncoming, relatedEntityArgs.get(key), arguments);
SingleValueArgumentEntry incoming = new SingleValueArgumentEntry(item); SingleValueArgumentEntry incoming = new SingleValueArgumentEntry(item);
mapLatest(incoming, args.get(key), arguments); mapLatest(ctx, incoming, args.get(key), arguments);
key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_ROLLING, null); key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_ROLLING, null);
mapRolling(item, args.get(key), arguments); mapRolling(item, args.get(key), arguments);
@ -598,7 +598,8 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
return arguments; return arguments;
} }
private void mapLatest(SingleValueArgumentEntry incoming, private void mapLatest(CalculatedFieldCtx ctx,
SingleValueArgumentEntry incoming,
Set<String> argNames, Set<String> argNames,
Map<String, ArgumentEntry> arguments) { Map<String, ArgumentEntry> arguments) {
if (argNames != null) { if (argNames != null) {
@ -606,7 +607,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (existing == null) { if (existing == null) {
return incoming; return incoming;
} }
existing.updateEntry(incoming); existing.updateEntry(incoming, ctx);
return existing; return existing;
})); }));
} }

2
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java

@ -52,7 +52,7 @@ public interface ArgumentEntry {
Object getValue(); Object getValue();
boolean updateEntry(ArgumentEntry entry); boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx);
boolean isEmpty(); boolean isEmpty();

8
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java

@ -86,13 +86,13 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState,
validateNewEntry(key, newEntry); validateNewEntry(key, newEntry);
if (existingEntry instanceof RelatedEntitiesArgumentEntry || if (existingEntry instanceof RelatedEntitiesArgumentEntry ||
existingEntry instanceof EntityAggregationArgumentEntry) { existingEntry instanceof EntityAggregationArgumentEntry) {
updateEntry(existingEntry, newEntry); updateEntry(existingEntry, newEntry, ctx);
} else { } else {
arguments.put(key, newEntry); arguments.put(key, newEntry);
} }
entryUpdated = true; entryUpdated = true;
} else { } else {
entryUpdated = updateEntry(existingEntry, newEntry); entryUpdated = updateEntry(existingEntry, newEntry, ctx);
} }
if (entryUpdated) { if (entryUpdated) {
@ -111,8 +111,8 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState,
return updatedArguments; return updatedArguments;
} }
protected boolean updateEntry(ArgumentEntry existingEntry, ArgumentEntry newEntry) { protected boolean updateEntry(ArgumentEntry existingEntry, ArgumentEntry newEntry, CalculatedFieldCtx ctx) {
return existingEntry.updateEntry(newEntry); return existingEntry.updateEntry(newEntry, ctx);
} }
@Override @Override

2
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java

@ -159,7 +159,7 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
} }
@Override @Override
public boolean updateEntry(ArgumentEntry entry) { public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) {
if (entry instanceof SingleValueArgumentEntry singleValueEntry) { if (entry instanceof SingleValueArgumentEntry singleValueEntry) {
if (singleValueEntry.getTs() < this.ts) { if (singleValueEntry.getTs() < this.ts) {
return false; return false;

2
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java

@ -100,7 +100,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry, HasLatestTs {
} }
@Override @Override
public boolean updateEntry(ArgumentEntry entry) { public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) {
if (entry instanceof TsRollingArgumentEntry tsRollingEntry) { if (entry instanceof TsRollingArgumentEntry tsRollingEntry) {
updateTsRollingEntry(tsRollingEntry); updateTsRollingEntry(tsRollingEntry);
} else if (entry instanceof SingleValueArgumentEntry singleValueEntry) { } else if (entry instanceof SingleValueArgumentEntry singleValueEntry) {

5
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java

@ -23,6 +23,7 @@ import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.HasLatestTs; import org.thingsboard.server.service.cf.ctx.state.HasLatestTs;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
@ -63,7 +64,7 @@ public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs
} }
@Override @Override
public boolean updateEntry(ArgumentEntry entry) { public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) {
if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) {
entityInputs.putAll(relatedEntitiesArgumentEntry.entityInputs); entityInputs.putAll(relatedEntitiesArgumentEntry.entityInputs);
return true; return true;
@ -74,7 +75,7 @@ public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs
} }
ArgumentEntry argumentEntry = entityInputs.get(singleValueArgumentEntry.getEntityId()); ArgumentEntry argumentEntry = entityInputs.get(singleValueArgumentEntry.getEntityId());
if (argumentEntry != null) { if (argumentEntry != null) {
argumentEntry.updateEntry(singleValueArgumentEntry); argumentEntry.updateEntry(singleValueArgumentEntry, ctx);
} else { } else {
entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry);
} }

68
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationArgumentEntry.java

@ -19,11 +19,18 @@ import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data; import lombok.Data;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.AggInterval;
import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.Watermark;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
@Data @Data
public class EntityAggregationArgumentEntry implements ArgumentEntry { public class EntityAggregationArgumentEntry implements ArgumentEntry {
@ -47,28 +54,63 @@ public class EntityAggregationArgumentEntry implements ArgumentEntry {
} }
@Override @Override
public boolean updateEntry(ArgumentEntry entry) { public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) {
boolean updated = false;
if (entry instanceof EntityAggregationArgumentEntry entityAggEntry) { if (entry instanceof EntityAggregationArgumentEntry entityAggEntry) {
aggIntervals.putAll(entityAggEntry.getAggIntervals()); aggIntervals.putAll(entityAggEntry.getAggIntervals());
return true;
} else if (entry instanceof SingleValueArgumentEntry singleValueArgEntry) { } else if (entry instanceof SingleValueArgumentEntry singleValueArgEntry) {
long entryTs = singleValueArgEntry.getTs(); long entryTs = singleValueArgEntry.getTs();
long argUpdateTs = System.currentTimeMillis(); long now = System.currentTimeMillis();
for (Map.Entry<AggIntervalEntry, AggIntervalEntryStatus> aggIntervalEntry : aggIntervals.entrySet()) { if (updateExistingIntervals(singleValueArgEntry, entryTs, now)) {
if (singleValueArgEntry.isForceResetPrevious()) { return true;
aggIntervalEntry.getValue().setLastArgsRefreshTs(argUpdateTs);
updated = true;
continue;
}
if (aggIntervalEntry.getKey().belongsToInterval(entryTs)) {
aggIntervalEntry.getValue().setLastArgsRefreshTs(argUpdateTs);
return true;
}
} }
return createNewInterval(entryTs, now, ctx);
} }
return false;
}
private boolean updateExistingIntervals(SingleValueArgumentEntry entry, long entryTs, long now) {
boolean updated = false;
for (Map.Entry<AggIntervalEntry, AggIntervalEntryStatus> aggIntervalEntry : aggIntervals.entrySet()) {
AggIntervalEntry interval = aggIntervalEntry.getKey();
AggIntervalEntryStatus status = aggIntervalEntry.getValue();
if (entry.isForceResetPrevious()) {
status.setLastArgsRefreshTs(now);
updated = true;
continue;
}
if (interval.belongsToInterval(entryTs)) {
status.setLastArgsRefreshTs(now);
return true;
}
}
return updated; return updated;
} }
private boolean createNewInterval(long entryTs, long now, CalculatedFieldCtx ctx) {
if (!(ctx.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration config)) {
return false;
}
AggInterval interval = config.getInterval();
Watermark watermark = config.getWatermark();
long watermarkDuration = watermark == null ? 0 : TimeUnit.SECONDS.toMillis(watermark.getDuration());
ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(entryTs), interval.getZoneId());
long startTs = interval.getDateTimeIntervalStartTs(zdt);
long endTs = interval.getDateTimeIntervalEndTs(zdt);
if (now - endTs > watermarkDuration) {
return false;
}
AggIntervalEntry newInterval = new AggIntervalEntry(startTs, endTs);
aggIntervals.computeIfAbsent(newInterval, i -> new AggIntervalEntryStatus(now));
return true;
}
@Override @Override
public boolean isEmpty() { public boolean isEmpty() {
return aggIntervals.isEmpty(); return aggIntervals.isEmpty();

9
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java

@ -154,8 +154,10 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
} }
private void fillMissingIntervals() { private void fillMissingIntervals() {
long now = System.currentTimeMillis();
ZoneId zoneId = interval.getZoneId(); ZoneId zoneId = interval.getZoneId();
long currentIntervalEndTs = interval.getCurrentIntervalEndTs(); long currentIntervalEndTs = interval.getCurrentIntervalEndTs();
long watermarkThresholdTs = now - watermarkDuration;
Map<AggIntervalEntry, Map<String, AggIntervalEntryStatus>> intervals = getIntervals(); Map<AggIntervalEntry, Map<String, AggIntervalEntryStatus>> intervals = getIntervals();
AggIntervalEntry lastIntervalEntry = intervals.keySet().stream().max(Comparator.comparing(AggIntervalEntry::getEndTs)).orElse(null); AggIntervalEntry lastIntervalEntry = intervals.keySet().stream().max(Comparator.comparing(AggIntervalEntry::getEndTs)).orElse(null);
@ -169,6 +171,13 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
while (nextEnd.toInstant().toEpochMilli() <= currentIntervalEndTs) { while (nextEnd.toInstant().toEpochMilli() <= currentIntervalEndTs) {
long nextStartTs = nextStart.toInstant().toEpochMilli(); long nextStartTs = nextStart.toInstant().toEpochMilli();
long nextEndTs = nextEnd.toInstant().toEpochMilli(); long nextEndTs = nextEnd.toInstant().toEpochMilli();
if (nextEndTs < watermarkThresholdTs) {
nextStart = nextEnd;
nextEnd = interval.getNextIntervalStart(nextStart);
continue;
}
AggIntervalEntry missing = new AggIntervalEntry(nextStartTs, nextEndTs); AggIntervalEntry missing = new AggIntervalEntry(nextStartTs, nextEndTs);
arguments.forEach((argName, argumentEntry) -> { arguments.forEach((argName, argumentEntry) -> {

9
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java

@ -150,7 +150,8 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
clearRuleState = null; clearRuleState = null;
} }
} }
log.debug("Initialized create rule states {} and clear rule state {} for {}", createRuleStates, clearRuleState, configuration); log.debug("Initialized create rule states {} and clear rule state {} for {}. Restored: {}, reeval needed: {}",
createRuleStates, clearRuleState, configuration, restored, reevalNeeded);
if (reevalNeeded.get()) { if (reevalNeeded.get()) {
initCurrentAlarm(ctx); initCurrentAlarm(ctx);
@ -224,17 +225,17 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
} }
@Override @Override
protected boolean updateEntry(ArgumentEntry existingArgumentEntry, ArgumentEntry newArgumentEntry) { protected boolean updateEntry(ArgumentEntry existingArgumentEntry, ArgumentEntry newArgumentEntry, CalculatedFieldCtx ctx) {
if (!(existingArgumentEntry instanceof SingleValueArgumentEntry existingEntry) || if (!(existingArgumentEntry instanceof SingleValueArgumentEntry existingEntry) ||
!(newArgumentEntry instanceof SingleValueArgumentEntry newEntry)) { !(newArgumentEntry instanceof SingleValueArgumentEntry newEntry)) {
return super.updateEntry(existingArgumentEntry, newArgumentEntry); return super.updateEntry(existingArgumentEntry, newArgumentEntry, ctx);
} }
if (newEntry.getTs() < existingEntry.getTs()) { if (newEntry.getTs() < existingEntry.getTs()) {
if (existingEntry.isDefaultValue()) { if (existingEntry.isDefaultValue()) {
existingEntry.setTs(newEntry.getTs()); existingEntry.setTs(newEntry.getTs());
} }
} }
return super.updateEntry(existingEntry, newEntry); return super.updateEntry(existingEntry, newEntry, ctx);
} }
public void processAlarmAction(Alarm alarm, ActionType action) { public void processAlarmAction(Alarm alarm, ActionType action) {

8
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java

@ -262,6 +262,14 @@ public class AlarmRuleState {
} }
} }
public void setDurationCheckFuture(ScheduledFuture<?> durationCheckFuture) {
if (this.durationCheckFuture != null) {
log.warn("Setting new duration check future while previous is not null for state {}", this, new RuntimeException("stacktrace"));
this.durationCheckFuture.cancel(true);
}
this.durationCheckFuture = durationCheckFuture;
}
public boolean isEmpty() { public boolean isEmpty() {
return eventCount == 0L && firstEventTs == 0L && lastCheckTs == 0L && durationCheckFuture == null; return eventCount == 0L && firstEventTs == 0L && lastCheckTs == 0L && durationCheckFuture == null;
} }

3
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.HasLatestTs; import org.thingsboard.server.service.cf.ctx.state.HasLatestTs;
import java.util.Map; import java.util.Map;
@ -68,7 +69,7 @@ public class GeofencingArgumentEntry implements ArgumentEntry, HasLatestTs {
} }
@Override @Override
public boolean updateEntry(ArgumentEntry entry) { public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) {
if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
throw new IllegalArgumentException("Unsupported argument entry type for geofencing argument entry: " + entry.getType()); throw new IllegalArgumentException("Unsupported argument entry type for geofencing argument entry: " + entry.getType());
} }

3
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java

@ -21,6 +21,7 @@ import org.thingsboard.script.api.tbel.TbelCfPropagationArg;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@ -59,7 +60,7 @@ public class PropagationArgumentEntry implements ArgumentEntry {
} }
@Override @Override
public boolean updateEntry(ArgumentEntry entry) { public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) {
if (!(entry instanceof PropagationArgumentEntry updated)) { if (!(entry instanceof PropagationArgumentEntry updated)) {
throw new IllegalArgumentException("Unsupported argument entry type for propagation argument entry: " + entry.getType()); throw new IllegalArgumentException("Unsupported argument entry type for propagation argument entry: " + entry.getType());
} }

44
application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java

@ -27,6 +27,7 @@ import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode; import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode;
import org.thingsboard.rule.engine.ai.TbAiNode;
import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode; import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode;
import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode; import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode;
import org.thingsboard.rule.engine.telemetry.TbCalculatedFieldsNode; import org.thingsboard.rule.engine.telemetry.TbCalculatedFieldsNode;
@ -167,6 +168,14 @@ public class EdgeMsgConstructorUtils {
); );
public static final Map<EdgeVersion, Set<String>> EXCLUDED_NODES_BY_EDGE_VERSION = Map.of( public static final Map<EdgeVersion, Set<String>> EXCLUDED_NODES_BY_EDGE_VERSION = Map.of(
EdgeVersion.V_4_1_0,
Set.of(
TbAiNode.class.getName()
),
EdgeVersion.V_4_0_0,
Set.of(
TbAiNode.class.getName()
),
EdgeVersion.V_3_9_0, EdgeVersion.V_3_9_0,
Set.of( Set.of(
TbCalculatedFieldsNode.class.getName() TbCalculatedFieldsNode.class.getName()
@ -560,7 +569,7 @@ public class EdgeMsgConstructorUtils {
.setEntityIdMSB(entityId.getId().getMostSignificantBits()) .setEntityIdMSB(entityId.getId().getMostSignificantBits())
.setEntityIdLSB(entityId.getId().getLeastSignificantBits()) .setEntityIdLSB(entityId.getId().getLeastSignificantBits())
.setEntityType(entityId.getEntityType().name()); .setEntityType(entityId.getEntityType().name());
long ts = getTs(entityData.getAsJsonObject()); long ts = extractTs(entityData.getAsJsonObject());
switch (actionType) { switch (actionType) {
case TIMESERIES_UPDATED: case TIMESERIES_UPDATED:
try { try {
@ -613,8 +622,8 @@ public class EdgeMsgConstructorUtils {
return builder.build(); return builder.build();
} }
private static long getTs(JsonObject data) { private static long extractTs(JsonObject data) {
if (data.get("ts") != null && !data.get("ts").isJsonNull()) { if (data.has("ts") && data.get("ts").isJsonPrimitive()) {
return data.getAsJsonPrimitive("ts").getAsLong(); return data.getAsJsonPrimitive("ts").getAsLong();
} }
return System.currentTimeMillis(); return System.currentTimeMillis();
@ -740,7 +749,7 @@ public class EdgeMsgConstructorUtils {
result.sort(Comparator.comparingLong(EdgeEvent::getSeqId)); result.sort(Comparator.comparingLong(EdgeEvent::getSeqId));
return result; return result;
} catch (Exception e) { } catch (Exception e) {
log.warn("Can't merge downlink duplicates, edgeEvents [{}]", edgeEvents, e); log.info("Can't merge downlink duplicates. Sending downlinks without merge. Original edgeEvents [{}]", edgeEvents, e);
return edgeEvents; return edgeEvents;
} }
} }
@ -751,6 +760,9 @@ public class EdgeMsgConstructorUtils {
} }
String bodyStr = JacksonUtil.toString(body); String bodyStr = JacksonUtil.toString(body);
var jsonObject = JsonParser.parseString(bodyStr).getAsJsonObject(); var jsonObject = JsonParser.parseString(bodyStr).getAsJsonObject();
if (!jsonObject.has("ts")) {
return new AttrsTs(0L, List.of());
}
long ts = jsonObject.get("ts").getAsLong(); long ts = jsonObject.get("ts").getAsLong();
var kv = jsonObject.getAsJsonObject("kv"); var kv = jsonObject.getAsJsonObject("kv");
List<AttributeKvEntry> attrs = JsonConverter.convertToAttributes( List<AttributeKvEntry> attrs = JsonConverter.convertToAttributes(
@ -761,22 +773,24 @@ public class EdgeMsgConstructorUtils {
} }
private static JsonNode filterAttributesBody(JsonNode body, Map<String, Long> latestByKey) { private static JsonNode filterAttributesBody(JsonNode body, Map<String, Long> latestByKey) {
if (body == null || latestByKey == null || latestByKey.isEmpty()) { if (body == null) {
return null; return null;
} }
String bodyStr = JacksonUtil.toString(body); String bodyStr = JacksonUtil.toString(body);
JsonObject jsonObject = JsonParser.parseString(bodyStr).getAsJsonObject(); JsonObject jsonObject = JsonParser.parseString(bodyStr).getAsJsonObject();
long ts = jsonObject.get("ts").getAsLong(); if (jsonObject.has("ts") && latestByKey != null && !latestByKey.isEmpty()) {
JsonObject kv = jsonObject.getAsJsonObject("kv"); long ts = jsonObject.get("ts").getAsLong();
for (Iterator<Map.Entry<String, JsonElement>> it = kv.entrySet().iterator(); it.hasNext(); ) { JsonObject kv = jsonObject.getAsJsonObject("kv");
Map.Entry<String, JsonElement> e = it.next(); for (Iterator<Map.Entry<String, JsonElement>> it = kv.entrySet().iterator(); it.hasNext(); ) {
Long latestTs = latestByKey.get(e.getKey()); Map.Entry<String, JsonElement> e = it.next();
if (latestTs == null || !latestTs.equals(ts)) { Long latestTs = latestByKey.get(e.getKey());
it.remove(); if (latestTs == null || !latestTs.equals(ts)) {
it.remove();
}
}
if (kv.isEmpty()) {
return null;
} }
}
if (kv.isEmpty()) {
return null;
} }
return JacksonUtil.toJsonNode(jsonObject.toString()); return JacksonUtil.toJsonNode(jsonObject.toString());
} }

3
application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java

@ -30,7 +30,8 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti
// This list should include all versions that are compatible for the upgrade in 4 digits format (like 4.2.0.0, etc.). // This list should include all versions that are compatible for the upgrade in 4 digits format (like 4.2.0.0, etc.).
// The compatibility cycle usually breaks when we have some scripts written in Java that may not work after a new release. // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after a new release.
private static final List<String> SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.1.0"); // TODO: don't check the "patch" number, since upgrade is not required for patch releases
private static final List<String> SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.1.0", "4.2.1.1", "4.2.1.2");
private final ProjectInfo projectInfo; private final ProjectInfo projectInfo;
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;

2
application/src/main/resources/thingsboard.yml

@ -1467,7 +1467,7 @@ device:
pem_cert_file: "${DEVICE_CONNECTIVITY_COAPS_CA_ROOT_CERT:cafile.pem}" pem_cert_file: "${DEVICE_CONNECTIVITY_COAPS_CA_ROOT_CERT:cafile.pem}"
gateway: gateway:
# The docker tag for thingsboard/tb-gateway image used in docker-compose file for gateway launch # The docker tag for thingsboard/tb-gateway image used in docker-compose file for gateway launch
image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:3.7-stable}" image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:3.8-stable}"
# Edges parameters # Edges parameters
edges: edges:

53
application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java

@ -55,6 +55,8 @@ import static org.thingsboard.server.cf.CalculatedFieldIntegrationTest.POLL_INTE
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest { public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest {
private final String TZ = "Europe/Kyiv";
private Tenant savedTenant; private Tenant savedTenant;
@Before @Before
@ -93,7 +95,7 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
public void testCreateCfAndNoTelemetryDuringInterval_checkAggregation() throws Exception { public void testCreateCfAndNoTelemetryDuringInterval_checkAggregation() throws Exception {
Device device = createDevice("Device", "1234567890111"); Device device = createDevice("Device", "1234567890111");
CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); CustomInterval customInterval = new CustomInterval(TZ, 0L, 5L);
createConsumptionCF(device.getId(), customInterval, null); createConsumptionCF(device.getId(), customInterval, null);
long interval = customInterval.getCurrentIntervalDurationMillis(); long interval = customInterval.getCurrentIntervalDurationMillis();
@ -113,7 +115,7 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
public void testCreateCfWithoutWatermark_checkAggregation() throws Exception { public void testCreateCfWithoutWatermark_checkAggregation() throws Exception {
Device device = createDevice("Device", "1234567890111"); Device device = createDevice("Device", "1234567890111");
CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); CustomInterval customInterval = new CustomInterval(TZ, 0L, 5L);
createConsumptionCF(device.getId(), customInterval, null); createConsumptionCF(device.getId(), customInterval, null);
long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs();
@ -156,7 +158,7 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
public void testCreateCfWithWatermark_checkAggregationDuringWatermark() throws Exception { public void testCreateCfWithWatermark_checkAggregationDuringWatermark() throws Exception {
Device device = createDevice("Device", "1234567890111"); Device device = createDevice("Device", "1234567890111");
CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); CustomInterval customInterval = new CustomInterval(TZ, 0L, 5L);
Watermark watermark = new Watermark(10); Watermark watermark = new Watermark(10);
createConsumptionCF(device.getId(), customInterval, watermark); createConsumptionCF(device.getId(), customInterval, watermark);
@ -196,6 +198,51 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
}); });
} }
@Test
public void testSendFutureTelemetry_checkAggregation() throws Exception {
Device device = createDevice("Device", "1234567890111");
CustomInterval customInterval = new CustomInterval(TZ, 0L, 2L);
createConsumptionCF(device.getId(), customInterval, null);
long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs();
long tsBeforeInterval = currentIntervalStartTs - 1000;
long tsInInterval_1 = currentIntervalStartTs + 1000;
long tsInInterval_2 = currentIntervalStartTs + 500;
long tsInInterval_3 = currentIntervalStartTs + 200;
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsBeforeInterval));
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":100}}", tsInInterval_1));
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180}}", tsInInterval_2));
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3));
long interval = customInterval.getCurrentIntervalDurationMillis();
await().alias("create CF -> perform aggregation after interval end")
.atMost(2 * interval, TimeUnit.MILLISECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption");
assertThat(result).isNotNull();
assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400");
assertThat(result.get("avgConsumption").get(0).get("value").asText()).isEqualTo("133");
});
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":500}}", currentIntervalStartTs + 4500L));
await().alias("update telemetry that belongs to future interval -> check aggregation ")
.atMost(3 * interval, TimeUnit.MILLISECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption");
assertThat(result).isNotNull();
assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("500");
assertThat(result.get("consumption").get(0).get("ts").asLong()).isEqualTo(currentIntervalStartTs + 4000L);
assertThat(result.get("avgConsumption").get(0).get("value").asText()).isEqualTo("500");
assertThat(result.get("avgConsumption").get(0).get("ts").asLong()).isEqualTo(currentIntervalStartTs + 4000L);
});
}
private CalculatedField createConsumptionCF(EntityId entityId, AggInterval aggInterval, Watermark watermark) { private CalculatedField createConsumptionCF(EntityId entityId, AggInterval aggInterval, Watermark watermark) {
Map<String, Argument> arguments = new HashMap<>(); Map<String, Argument> arguments = new HashMap<>();
Argument argument = new Argument(); Argument argument = new Argument();

2
application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java

@ -95,7 +95,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
private DeviceProfileId mqttDeviceProfileId; private DeviceProfileId mqttDeviceProfileId;
private DeviceProfileId coapDeviceProfileId; private DeviceProfileId coapDeviceProfileId;
@Value("${device.connectivity.gateway.image_version:3.7-stable}") @Value("${device.connectivity.gateway.image_version:3.8-stable}")
private String gatewayImageVersion; private String gatewayImageVersion;
@Before @Before

5
application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java

@ -49,8 +49,6 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
@ -671,8 +669,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
.atMost(10, TimeUnit.SECONDS) .atMost(10, TimeUnit.SECONDS)
.until(() -> { .until(() -> {
String urlTemplate = "/api/plugins/telemetry/DEVICE/" + device.getId() + "/keys/attributes/" + scope; String urlTemplate = "/api/plugins/telemetry/DEVICE/" + device.getId() + "/keys/attributes/" + scope;
List<String> actualKeys = doGetAsyncTyped(urlTemplate, new TypeReference<>() { List<String> actualKeys = doGetAsyncTyped(urlTemplate, new TypeReference<>() {});
});
return actualKeys != null && !actualKeys.isEmpty() && actualKeys.contains(expectedKey); return actualKeys != null && !actualKeys.isEmpty() && actualKeys.contains(expectedKey);
}); });

25
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java

@ -18,6 +18,9 @@ package org.thingsboard.server.service.cf.ctx.state;
import io.hypersistence.utils.hibernate.type.json.internal.JacksonUtil; import io.hypersistence.utils.hibernate.type.json.internal.JacksonUtil;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.geo.PerimeterDefinition; import org.thingsboard.common.util.geo.PerimeterDefinition;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -33,6 +36,7 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ExtendWith(MockitoExtension.class)
public class GeofencingValueArgumentEntryTest { public class GeofencingValueArgumentEntryTest {
private final AssetId ZONE_1_ID = new AssetId(UUID.fromString("c0e3031c-7df1-45e4-9590-cfd621a4d714")); private final AssetId ZONE_1_ID = new AssetId(UUID.fromString("c0e3031c-7df1-45e4-9590-cfd621a4d714"));
@ -46,6 +50,9 @@ public class GeofencingValueArgumentEntryTest {
private GeofencingArgumentEntry entry; private GeofencingArgumentEntry entry;
@Mock
private CalculatedFieldCtx ctx;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
entry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry, ZONE_2_ID, restrictedZoneAttributeKvEntry)); entry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry, ZONE_2_ID, restrictedZoneAttributeKvEntry));
@ -58,14 +65,14 @@ public class GeofencingValueArgumentEntryTest {
@Test @Test
void testUpdateEntryWhenSingleEntryPassed() { void testUpdateEntryWhenSingleEntryPassed() {
assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry())) assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry(), ctx))
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported argument entry type for geofencing argument entry: SINGLE_VALUE"); .hasMessage("Unsupported argument entry type for geofencing argument entry: SINGLE_VALUE");
} }
@Test @Test
void testUpdateEntryWhenRollingEntryPassed() { void testUpdateEntryWhenRollingEntryPassed() {
assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L), ctx))
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported argument entry type for geofencing argument entry: TS_ROLLING"); .hasMessage("Unsupported argument entry type for geofencing argument entry: TS_ROLLING");
} }
@ -74,7 +81,7 @@ public class GeofencingValueArgumentEntryTest {
void testUpdateEntryWithTheSameTs() { void testUpdateEntryWithTheSameTs() {
BaseAttributeKvEntry differentValueSameTs = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 363L, 156L); BaseAttributeKvEntry differentValueSameTs = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 363L, 156L);
var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueSameTs, ZONE_2_ID, restrictedZoneAttributeKvEntry)); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueSameTs, ZONE_2_ID, restrictedZoneAttributeKvEntry));
assertThat(entry.updateEntry(updated)).isFalse(); assertThat(entry.updateEntry(updated, ctx)).isFalse();
} }
@Test @Test
@ -83,7 +90,7 @@ public class GeofencingValueArgumentEntryTest {
BaseAttributeKvEntry differentValueNewVersionIsNull = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, null); BaseAttributeKvEntry differentValueNewVersionIsNull = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, null);
var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsNull, ZONE_2_ID, restrictedZoneAttributeKvEntry)); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsNull, ZONE_2_ID, restrictedZoneAttributeKvEntry));
assertThat(entry.updateEntry(updated)).isTrue(); assertThat(entry.updateEntry(updated, ctx)).isTrue();
assertThat(entry.getValue()).isInstanceOf(Map.class); assertThat(entry.getValue()).isInstanceOf(Map.class);
Map<EntityId, GeofencingZoneState> value = (Map<EntityId, GeofencingZoneState>) entry.getValue(); Map<EntityId, GeofencingZoneState> value = (Map<EntityId, GeofencingZoneState>) entry.getValue();
@ -105,7 +112,7 @@ public class GeofencingValueArgumentEntryTest {
BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 156L); BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 156L);
var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsSet, ZONE_2_ID, restrictedZoneAttributeKvEntry)); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsSet, ZONE_2_ID, restrictedZoneAttributeKvEntry));
assertThat(entry.updateEntry(updated)).isTrue(); assertThat(entry.updateEntry(updated, ctx)).isTrue();
assertThat(entry.getValue()).isInstanceOf(Map.class); assertThat(entry.getValue()).isInstanceOf(Map.class);
Map<EntityId, GeofencingZoneState> value = (Map<EntityId, GeofencingZoneState>) entry.getValue(); Map<EntityId, GeofencingZoneState> value = (Map<EntityId, GeofencingZoneState>) entry.getValue();
@ -126,7 +133,7 @@ public class GeofencingValueArgumentEntryTest {
BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 154L); BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 154L);
var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsSet, ZONE_2_ID, restrictedZoneAttributeKvEntry)); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsSet, ZONE_2_ID, restrictedZoneAttributeKvEntry));
assertThat(entry.updateEntry(updated)).isFalse(); assertThat(entry.updateEntry(updated, ctx)).isFalse();
} }
@Test @Test
@ -134,7 +141,7 @@ public class GeofencingValueArgumentEntryTest {
BaseAttributeKvEntry newTsAndTheSameValue = new BaseAttributeKvEntry(allowedZoneDataEntry, 364L, 156L); BaseAttributeKvEntry newTsAndTheSameValue = new BaseAttributeKvEntry(allowedZoneDataEntry, 364L, 156L);
var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, newTsAndTheSameValue, ZONE_2_ID, restrictedZoneAttributeKvEntry)); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, newTsAndTheSameValue, ZONE_2_ID, restrictedZoneAttributeKvEntry));
assertThat(entry.updateEntry(updated)).isTrue(); assertThat(entry.updateEntry(updated, ctx)).isTrue();
} }
@Test @Test
@ -142,7 +149,7 @@ public class GeofencingValueArgumentEntryTest {
BaseAttributeKvEntry oldTsAndTheSameValue = new BaseAttributeKvEntry(allowedZoneDataEntry, 362L, 156L); BaseAttributeKvEntry oldTsAndTheSameValue = new BaseAttributeKvEntry(allowedZoneDataEntry, 362L, 156L);
var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, oldTsAndTheSameValue, ZONE_2_ID, restrictedZoneAttributeKvEntry)); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, oldTsAndTheSameValue, ZONE_2_ID, restrictedZoneAttributeKvEntry));
assertThat(entry.updateEntry(updated)).isFalse(); assertThat(entry.updateEntry(updated, ctx)).isFalse();
} }
@Test @Test
@ -150,7 +157,7 @@ public class GeofencingValueArgumentEntryTest {
final AssetId NEW_ZONE_ID = new AssetId(UUID.fromString("a3eacf1a-6af3-4e9f-87c4-502bb25c7dc3")); final AssetId NEW_ZONE_ID = new AssetId(UUID.fromString("a3eacf1a-6af3-4e9f-87c4-502bb25c7dc3"));
BaseAttributeKvEntry newZone = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 156L); BaseAttributeKvEntry newZone = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 156L);
var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry, ZONE_2_ID, restrictedZoneAttributeKvEntry, NEW_ZONE_ID, newZone)); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry, ZONE_2_ID, restrictedZoneAttributeKvEntry, NEW_ZONE_ID, newZone));
assertThat(entry.updateEntry(updated)).isTrue(); assertThat(entry.updateEntry(updated, ctx)).isTrue();
} }
@Test @Test

33
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java

@ -17,6 +17,9 @@ package org.thingsboard.server.service.cf.ctx.state;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfPropagationArg; import org.thingsboard.script.api.tbel.TbelCfPropagationArg;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
@ -31,6 +34,7 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ExtendWith(MockitoExtension.class)
public class PropagationArgumentEntryTest { public class PropagationArgumentEntryTest {
private final AssetId ENTITY_1_ID = new AssetId(UUID.fromString("b0a8637d-6d67-43d5-a483-c0e391afe805")); private final AssetId ENTITY_1_ID = new AssetId(UUID.fromString("b0a8637d-6d67-43d5-a483-c0e391afe805"));
@ -39,6 +43,9 @@ public class PropagationArgumentEntryTest {
private PropagationArgumentEntry entry; private PropagationArgumentEntry entry;
@Mock
private CalculatedFieldCtx ctx;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
List<EntityId> propagationEntityIds = new ArrayList<>(); List<EntityId> propagationEntityIds = new ArrayList<>();
@ -68,14 +75,14 @@ public class PropagationArgumentEntryTest {
@Test @Test
void testUpdateEntryWhenSingleEntryPassed() { void testUpdateEntryWhenSingleEntryPassed() {
assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry())) assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry(), ctx))
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported argument entry type for propagation argument entry: SINGLE_VALUE"); .hasMessage("Unsupported argument entry type for propagation argument entry: SINGLE_VALUE");
} }
@Test @Test
void testUpdateEntryWhenRollingEntryPassed() { void testUpdateEntryWhenRollingEntryPassed() {
assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L), ctx))
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported argument entry type for propagation argument entry: TS_ROLLING"); .hasMessage("Unsupported argument entry type for propagation argument entry: TS_ROLLING");
} }
@ -85,7 +92,7 @@ public class PropagationArgumentEntryTest {
var newIds = new ArrayList<EntityId>(List.of(ENTITY_3_ID, ENTITY_1_ID)); var newIds = new ArrayList<EntityId>(List.of(ENTITY_3_ID, ENTITY_1_ID));
var updated = new PropagationArgumentEntry(newIds); var updated = new PropagationArgumentEntry(newIds);
boolean changed = entry.updateEntry(updated); boolean changed = entry.updateEntry(updated, ctx);
assertThat(changed).isTrue(); assertThat(changed).isTrue();
assertThat(entry.getEntityIds()).containsExactlyElementsOf(newIds); assertThat(entry.getEntityIds()).containsExactlyElementsOf(newIds);
@ -95,7 +102,7 @@ public class PropagationArgumentEntryTest {
void testUpdateEntryClearsWhenNewEntryIsEmpty() { void testUpdateEntryClearsWhenNewEntryIsEmpty() {
var updatedEmpty = new PropagationArgumentEntry(List.of()); var updatedEmpty = new PropagationArgumentEntry(List.of());
boolean changed = entry.updateEntry(updatedEmpty); boolean changed = entry.updateEntry(updatedEmpty, ctx);
assertThat(changed).isTrue(); assertThat(changed).isTrue();
assertThat(entry.getEntityIds()).isEmpty(); assertThat(entry.getEntityIds()).isEmpty();
@ -106,7 +113,7 @@ public class PropagationArgumentEntryTest {
var added = new PropagationArgumentEntry(); var added = new PropagationArgumentEntry();
added.setAdded(List.of(ENTITY_3_ID)); added.setAdded(List.of(ENTITY_3_ID));
boolean changed = entry.updateEntry(added); boolean changed = entry.updateEntry(added, ctx);
assertThat(changed).isTrue(); assertThat(changed).isTrue();
assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID); assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID);
@ -118,7 +125,7 @@ public class PropagationArgumentEntryTest {
var added = new PropagationArgumentEntry(); var added = new PropagationArgumentEntry();
added.setAdded(List.of(ENTITY_2_ID)); added.setAdded(List.of(ENTITY_2_ID));
boolean changed = entry.updateEntry(added); boolean changed = entry.updateEntry(added, ctx);
assertThat(changed).isFalse(); assertThat(changed).isFalse();
assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID); assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID);
@ -130,7 +137,7 @@ public class PropagationArgumentEntryTest {
var removed = new PropagationArgumentEntry(); var removed = new PropagationArgumentEntry();
removed.setRemoved(ENTITY_2_ID); removed.setRemoved(ENTITY_2_ID);
boolean changed = entry.updateEntry(removed); boolean changed = entry.updateEntry(removed, ctx);
assertThat(changed).isTrue(); assertThat(changed).isTrue();
assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID); assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID);
@ -142,7 +149,7 @@ public class PropagationArgumentEntryTest {
var removed = new PropagationArgumentEntry(); var removed = new PropagationArgumentEntry();
removed.setRemoved(ENTITY_3_ID); removed.setRemoved(ENTITY_3_ID);
boolean changed = entry.updateEntry(removed); boolean changed = entry.updateEntry(removed, ctx);
assertThat(changed).isFalse(); assertThat(changed).isFalse();
assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID); assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID);
@ -154,7 +161,7 @@ public class PropagationArgumentEntryTest {
var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID)); var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID));
restore.setIgnoreRemovedEntities(true); restore.setIgnoreRemovedEntities(true);
boolean changed = entry.updateEntry(restore); boolean changed = entry.updateEntry(restore, ctx);
assertThat(changed).isTrue(); assertThat(changed).isTrue();
assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID); assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID);
@ -168,7 +175,7 @@ public class PropagationArgumentEntryTest {
var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID)); var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID));
restore.setIgnoreRemovedEntities(true); restore.setIgnoreRemovedEntities(true);
boolean changed = entry.updateEntry(restore); boolean changed = entry.updateEntry(restore, ctx);
assertThat(changed).isFalse(); // expected no change, since we consider the removal of stale ids as no-op assertThat(changed).isFalse(); // expected no change, since we consider the removal of stale ids as no-op
assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID); assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID);
@ -182,7 +189,7 @@ public class PropagationArgumentEntryTest {
var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_3_ID)); var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_3_ID));
restore.setIgnoreRemovedEntities(true); restore.setIgnoreRemovedEntities(true);
boolean changed = entry.updateEntry(restore); boolean changed = entry.updateEntry(restore, ctx);
assertThat(changed).isTrue(); assertThat(changed).isTrue();
assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_3_ID); assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_3_ID);
@ -197,7 +204,7 @@ public class PropagationArgumentEntryTest {
var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_2_ID)); var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_2_ID));
restore.setIgnoreRemovedEntities(true); restore.setIgnoreRemovedEntities(true);
boolean changed = entry.updateEntry(restore); boolean changed = entry.updateEntry(restore, ctx);
assertThat(changed).isFalse(); assertThat(changed).isFalse();
assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID); assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID);
@ -211,7 +218,7 @@ public class PropagationArgumentEntryTest {
var restore = new PropagationArgumentEntry(List.of()); var restore = new PropagationArgumentEntry(List.of());
restore.setIgnoreRemovedEntities(true); restore.setIgnoreRemovedEntities(true);
boolean changed = entry.updateEntry(restore); boolean changed = entry.updateEntry(restore, ctx);
assertThat(changed).isFalse(); // expected no change, since we consider the removal of stale ids as no-op assertThat(changed).isFalse(); // expected no change, since we consider the removal of stale ids as no-op
assertThat(entry.getEntityIds()).isEmpty(); assertThat(entry.getEntityIds()).isEmpty();

15
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java

@ -17,6 +17,9 @@ package org.thingsboard.server.service.cf.ctx.state;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
@ -30,10 +33,14 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ExtendWith(MockitoExtension.class)
public class RelatedEntitiesArgumentEntryTest { public class RelatedEntitiesArgumentEntryTest {
private RelatedEntitiesArgumentEntry entry; private RelatedEntitiesArgumentEntry entry;
@Mock
private CalculatedFieldCtx ctx;
private final DeviceId device1 = new DeviceId(UUID.fromString("1984e5f4-9ff0-4187-84ae-e4438bba4c8a")); private final DeviceId device1 = new DeviceId(UUID.fromString("1984e5f4-9ff0-4187-84ae-e4438bba4c8a"));
private final DeviceId device2 = new DeviceId(UUID.fromString("937fc062-1a9d-438f-aa22-55a93fc908b7")); private final DeviceId device2 = new DeviceId(UUID.fromString("937fc062-1a9d-438f-aa22-55a93fc908b7"));
@ -50,7 +57,7 @@ public class RelatedEntitiesArgumentEntryTest {
@Test @Test
void testUpdateEntryWhenNotAggEntryPassed() { void testUpdateEntryWhenNotAggEntryPassed() {
assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L), ctx))
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported argument entry type for aggregation argument entry: " + ArgumentEntryType.TS_ROLLING); .hasMessage("Unsupported argument entry type for aggregation argument entry: " + ArgumentEntryType.TS_ROLLING);
} }
@ -65,7 +72,7 @@ public class RelatedEntitiesArgumentEntryTest {
device4, new SingleValueArgumentEntry(device4, new BasicTsKvEntry(ts - 60, new LongDataEntry("key", 23L), 7L)) device4, new SingleValueArgumentEntry(device4, new BasicTsKvEntry(ts - 60, new LongDataEntry("key", 23L), 7L))
), false); ), false);
assertThat(entry.updateEntry(relatedEntitiesArgumentEntry)).isTrue(); assertThat(entry.updateEntry(relatedEntitiesArgumentEntry, ctx)).isTrue();
Map<EntityId, ArgumentEntry> aggInputs = entry.getEntityInputs(); Map<EntityId, ArgumentEntry> aggInputs = entry.getEntityInputs();
assertThat(aggInputs.size()).isEqualTo(4); assertThat(aggInputs.size()).isEqualTo(4);
@ -79,7 +86,7 @@ public class RelatedEntitiesArgumentEntryTest {
SingleValueArgumentEntry singleEntityArgumentEntry = new SingleValueArgumentEntry(device3, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 18L), 10L)); SingleValueArgumentEntry singleEntityArgumentEntry = new SingleValueArgumentEntry(device3, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 18L), 10L));
assertThat(entry.updateEntry(singleEntityArgumentEntry)).isTrue(); assertThat(entry.updateEntry(singleEntityArgumentEntry, ctx)).isTrue();
Map<EntityId, ArgumentEntry> aggInputs = entry.getEntityInputs(); Map<EntityId, ArgumentEntry> aggInputs = entry.getEntityInputs();
assertThat(aggInputs.size()).isEqualTo(3); assertThat(aggInputs.size()).isEqualTo(3);
@ -90,7 +97,7 @@ public class RelatedEntitiesArgumentEntryTest {
void testUpdateEntryWhenSingleValueArgumentEntryPassedAndEntryByIdExist() { void testUpdateEntryWhenSingleValueArgumentEntryPassedAndEntryByIdExist() {
SingleValueArgumentEntry singleEntityArgumentEntry = new SingleValueArgumentEntry(device2, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 18L), 10L)); SingleValueArgumentEntry singleEntityArgumentEntry = new SingleValueArgumentEntry(device2, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 18L), 10L));
assertThat(entry.updateEntry(singleEntityArgumentEntry)).isTrue(); assertThat(entry.updateEntry(singleEntityArgumentEntry, ctx)).isTrue();
Map<EntityId, ArgumentEntry> aggInputs = entry.getEntityInputs(); Map<EntityId, ArgumentEntry> aggInputs = entry.getEntityInputs();
assertThat(aggInputs.size()).isEqualTo(2); assertThat(aggInputs.size()).isEqualTo(2);

23
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java

@ -17,6 +17,9 @@ package org.thingsboard.server.service.cf.ctx.state;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry;
@ -29,10 +32,14 @@ import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ExtendWith(MockitoExtension.class)
public class SingleValueArgumentEntryTest { public class SingleValueArgumentEntryTest {
private SingleValueArgumentEntry entry; private SingleValueArgumentEntry entry;
@Mock
private CalculatedFieldCtx ctx;
private final long ts = System.currentTimeMillis(); private final long ts = System.currentTimeMillis();
@BeforeEach @BeforeEach
@ -47,48 +54,48 @@ public class SingleValueArgumentEntryTest {
@Test @Test
void testUpdateEntryWhenRollingEntryPassed() { void testUpdateEntryWhenRollingEntryPassed() {
assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L), ctx))
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported argument entry type for single value argument entry: " + ArgumentEntryType.TS_ROLLING); .hasMessage("Unsupported argument entry type for single value argument entry: " + ArgumentEntryType.TS_ROLLING);
} }
@Test @Test
void testUpdateEntryWithTheSameTs() { void testUpdateEntryWithTheSameTs() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, new LongDataEntry("key", 13L), 363L))).isFalse(); assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, new LongDataEntry("key", 13L), 363L), ctx)).isFalse();
} }
@Test @Test
void testUpdateEntryWithTheSameTsAndDifferentVersion() { void testUpdateEntryWithTheSameTsAndDifferentVersion() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, new LongDataEntry("key", 13L), 364L))).isTrue(); assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, new LongDataEntry("key", 13L), 364L), ctx)).isTrue();
} }
@Test @Test
void testUpdateEntryWhenNewVersionIsNull() { void testUpdateEntryWhenNewVersionIsNull() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 16, new LongDataEntry("key", 13L), null))).isTrue(); assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 16, new LongDataEntry("key", 13L), null), ctx)).isTrue();
assertThat(entry.getValue()).isEqualTo(13L); assertThat(entry.getValue()).isEqualTo(13L);
assertThat(entry.getVersion()).isNull(); assertThat(entry.getVersion()).isNull();
} }
@Test @Test
void testUpdateEntryWhenNewVersionIsGreaterThanCurrent() { void testUpdateEntryWhenNewVersionIsGreaterThanCurrent() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 18L), 369L))).isTrue(); assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 18L), 369L), ctx)).isTrue();
assertThat(entry.getValue()).isEqualTo(18L); assertThat(entry.getValue()).isEqualTo(18L);
assertThat(entry.getVersion()).isEqualTo(369L); assertThat(entry.getVersion()).isEqualTo(369L);
} }
@Test @Test
void testUpdateEntryWhenNewVersionIsLessThanCurrent() { void testUpdateEntryWhenNewVersionIsLessThanCurrent() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 18L), 234L))).isFalse(); assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 18L), 234L), ctx)).isFalse();
} }
@Test @Test
void testUpdateEntryWhenValueWasNotChanged() { void testUpdateEntryWhenValueWasNotChanged() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 364L))).isTrue(); assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 364L), ctx)).isTrue();
} }
@Test @Test
void testUpdateEntryWithOldTs() { void testUpdateEntryWithOldTs() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts - 10, new LongDataEntry("key", 14L), 365L))).isFalse(); assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts - 10, new LongDataEntry("key", 14L), 365L), ctx)).isFalse();
} }
@Test @Test

17
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java

@ -17,6 +17,9 @@ package org.thingsboard.server.service.cf.ctx.state;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
@ -26,10 +29,14 @@ import java.util.TreeMap;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ExtendWith(MockitoExtension.class)
public class TsRollingArgumentEntryTest { public class TsRollingArgumentEntryTest {
private TsRollingArgumentEntry entry; private TsRollingArgumentEntry entry;
@Mock
private CalculatedFieldCtx ctx;
private final long ts = System.currentTimeMillis(); private final long ts = System.currentTimeMillis();
@BeforeEach @BeforeEach
@ -51,7 +58,7 @@ public class TsRollingArgumentEntryTest {
void testUpdateEntryWhenSingleValueEntryPassed() { void testUpdateEntryWhenSingleValueEntryPassed() {
SingleValueArgumentEntry newEntry = new SingleValueArgumentEntry(ts - 10, new DoubleDataEntry("key", 23.0), 123L); SingleValueArgumentEntry newEntry = new SingleValueArgumentEntry(ts - 10, new DoubleDataEntry("key", 23.0), 123L);
assertThat(entry.updateEntry(newEntry)).isTrue(); assertThat(entry.updateEntry(newEntry, ctx)).isTrue();
assertThat(entry.getTsRecords()).hasSize(4); assertThat(entry.getTsRecords()).hasSize(4);
assertThat(entry.getTsRecords().get(ts - 10)).isEqualTo(23.0); assertThat(entry.getTsRecords().get(ts - 10)).isEqualTo(23.0);
} }
@ -64,7 +71,7 @@ public class TsRollingArgumentEntryTest {
values.put(ts - 5, 1.0); values.put(ts - 5, 1.0);
newEntry.setTsRecords(values); newEntry.setTsRecords(values);
assertThat(entry.updateEntry(newEntry)).isTrue(); assertThat(entry.updateEntry(newEntry, ctx)).isTrue();
assertThat(entry.getTsRecords()).hasSize(5); assertThat(entry.getTsRecords()).hasSize(5);
assertThat(entry.getTsRecords()).isEqualTo(Map.of( assertThat(entry.getTsRecords()).isEqualTo(Map.of(
ts - 40, 10.0, ts - 40, 10.0,
@ -79,7 +86,7 @@ public class TsRollingArgumentEntryTest {
void testUpdateEntryWhenValueIsNotNumber() { void testUpdateEntryWhenValueIsNotNumber() {
SingleValueArgumentEntry newEntry = new SingleValueArgumentEntry(ts - 10, new StringDataEntry("key", "string"), 123L); SingleValueArgumentEntry newEntry = new SingleValueArgumentEntry(ts - 10, new StringDataEntry("key", "string"), 123L);
assertThat(entry.updateEntry(newEntry)).isTrue(); assertThat(entry.updateEntry(newEntry, ctx)).isTrue();
assertThat(entry.getTsRecords().get(ts - 10)).isNaN(); assertThat(entry.getTsRecords().get(ts - 10)).isNaN();
} }
@ -93,7 +100,7 @@ public class TsRollingArgumentEntryTest {
newEntry.setTsRecords(values); newEntry.setTsRecords(values);
entry = new TsRollingArgumentEntry(3, 30000L); entry = new TsRollingArgumentEntry(3, 30000L);
assertThat(entry.updateEntry(newEntry)).isTrue(); assertThat(entry.updateEntry(newEntry, ctx)).isTrue();
assertThat(entry.getTsRecords()).hasSize(1); assertThat(entry.getTsRecords()).hasSize(1);
assertThat(entry.getTsRecords()).isEqualTo(Map.of( assertThat(entry.getTsRecords()).isEqualTo(Map.of(
ts - 5, 0.0 ts - 5, 0.0
@ -111,7 +118,7 @@ public class TsRollingArgumentEntryTest {
newEntry.setTsRecords(values); newEntry.setTsRecords(values);
entry = new TsRollingArgumentEntry(3, 30000L); entry = new TsRollingArgumentEntry(3, 30000L);
assertThat(entry.updateEntry(newEntry)).isTrue(); assertThat(entry.updateEntry(newEntry, ctx)).isTrue();
assertThat(entry.getTsRecords()).hasSize(3); assertThat(entry.getTsRecords()).hasSize(3);
assertThat(entry.getTsRecords()).isEqualTo(Map.of( assertThat(entry.getTsRecords()).isEqualTo(Map.of(
ts - 18, 0.0, ts - 18, 0.0,

25
application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java

@ -230,10 +230,35 @@ public class EdgeMsgConstructorUtilsTest {
Assertions.assertEquals(8, getIntValue(assetMergedAttrAD.getBody(), "d")); Assertions.assertEquals(8, getIntValue(assetMergedAttrAD.getBody(), "d"));
} }
@Test
public void testMergeDownlinkDuplicates_attrBodyHasNoTs_returnOriginalList() {
UUID deviceId = UUID.randomUUID();
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
var deviceAttrUpdate1 = createEdgeEvent(tenantId, 1, EdgeEventActionType.ATTRIBUTES_UPDATED,
deviceId, EdgeEventType.DEVICE, createAttrBodyWithoutTs("{\"a\":1,\"b\":1,\"d\":1}"));
var deviceAttrUpdate2 = createEdgeEvent(tenantId, 2, EdgeEventActionType.ATTRIBUTES_UPDATED,
deviceId, EdgeEventType.DEVICE, createAttrBodyWithoutTs("{\"a\":2,\"b\":2,\"c\":2}"));
var deviceAttrUpdate3 = createEdgeEvent(tenantId, 3, EdgeEventActionType.ATTRIBUTES_UPDATED,
deviceId, EdgeEventType.DEVICE, createAttrBodyWithoutTs("{\"a\":3,\"d\":3}"));
List<EdgeEvent> input = List.of(deviceAttrUpdate1, deviceAttrUpdate2, deviceAttrUpdate3);
List<EdgeEvent> merged = EdgeMsgConstructorUtils.mergeAndFilterDownlinkDuplicates(input);
Assertions.assertEquals(3, merged.size());
Assertions.assertEquals(deviceAttrUpdate1, merged.get(0));
Assertions.assertEquals(deviceAttrUpdate2, merged.get(1));
Assertions.assertEquals(deviceAttrUpdate3, merged.get(2));
}
private Integer getIntValue(JsonNode body, String key) { private Integer getIntValue(JsonNode body, String key) {
return body.get("kv").get(key) != null ? body.get("kv").get(key).asInt() : null; return body.get("kv").get(key) != null ? body.get("kv").get(key).asInt() : null;
} }
private static JsonNode createAttrBodyWithoutTs(String kvJson) {
return JacksonUtil.toJsonNode("{\"kv\":" + kvJson + "}");
}
private static JsonNode createAttrBody(long ts, String kvJson) { private static JsonNode createAttrBody(long ts, String kvJson) {
return JacksonUtil.toJsonNode("{\"ts\":" + ts + ",\"kv\":" + kvJson + "}"); return JacksonUtil.toJsonNode("{\"ts\":" + ts + ",\"kv\":" + kvJson + "}");
} }

21
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java

@ -289,6 +289,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
@Override @Override
public String getObjectIdByKeyNameFromProfile(LwM2mClient client, String keyName) { public String getObjectIdByKeyNameFromProfile(LwM2mClient client, String keyName) {
Lwm2mDeviceProfileTransportConfiguration profile = getProfile(client.getRegistration()); Lwm2mDeviceProfileTransportConfiguration profile = getProfile(client.getRegistration());
if (profile == null) throw new IllegalArgumentException(keyName + " is not configured in the device profile! Device profile is null");
for (Map.Entry<String, String> entry : profile.getObserveAttr().getKeyName().entrySet()) { for (Map.Entry<String, String> entry : profile.getObserveAttr().getKeyName().entrySet()) {
String k = entry.getKey(); String k = entry.getKey();
String v = entry.getValue(); String v = entry.getValue();
@ -347,6 +348,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
PowerMode powerMode = lwM2MClient.getPowerMode(); PowerMode powerMode = lwM2MClient.getPowerMode();
if (powerMode == null) { if (powerMode == null) {
Lwm2mDeviceProfileTransportConfiguration deviceProfile = getProfile(lwM2MClient.getRegistration()); Lwm2mDeviceProfileTransportConfiguration deviceProfile = getProfile(lwM2MClient.getRegistration());
if (deviceProfile == null) return null;
powerMode = deviceProfile.getClientLwM2mSettings().getPowerMode(); powerMode = deviceProfile.getClientLwM2mSettings().getPowerMode();
} }
return powerMode; return powerMode;
@ -377,8 +379,8 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
} }
} }
} else { } else {
log.warn("Device profile not found! The device profile ID is null. Return Lwm2mDeviceProfileTransportConfiguration with default."); log.warn("Device profile not found! The device profile ID is null. Return Lwm2mDeviceProfileTransportConfiguration with null.");
result = new Lwm2mDeviceProfileTransportConfiguration(); result = null;
} }
return result; return result;
} }
@ -413,6 +415,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
OtherConfiguration profileSettings = null; OtherConfiguration profileSettings = null;
if (powerMode == null && client.getProfileId() != null) { if (powerMode == null && client.getProfileId() != null) {
var clientProfile = getProfile(client.getRegistration()); var clientProfile = getProfile(client.getRegistration());
if (clientProfile == null) return true;
profileSettings = clientProfile.getClientLwM2mSettings(); profileSettings = clientProfile.getClientLwM2mSettings();
powerMode = profileSettings.getPowerMode(); powerMode = profileSettings.getPowerMode();
} }
@ -459,8 +462,10 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
OtherConfiguration profileSettings = null; OtherConfiguration profileSettings = null;
if (powerMode == null && client.getProfileId() != null) { if (powerMode == null && client.getProfileId() != null) {
var clientProfile = getProfile(client.getRegistration()); var clientProfile = getProfile(client.getRegistration());
profileSettings = clientProfile.getClientLwM2mSettings(); if (clientProfile != null) {
powerMode = profileSettings.getPowerMode(); profileSettings = clientProfile.getClientLwM2mSettings();
powerMode = profileSettings.getPowerMode();
}
} }
if (powerMode == null || PowerMode.DRX.equals(powerMode)) { if (powerMode == null || PowerMode.DRX.equals(powerMode)) {
client.updateLastUplinkTime(); client.updateLastUplinkTime();
@ -515,9 +520,11 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
timeout = client.getEdrxCycle(); timeout = client.getEdrxCycle();
} else { } else {
var clientProfile = getProfile(client.getRegistration()); var clientProfile = getProfile(client.getRegistration());
OtherConfiguration clientLwM2mSettings = clientProfile.getClientLwM2mSettings(); if (clientProfile != null) {
if (PowerMode.E_DRX.equals(clientLwM2mSettings.getPowerMode())) { OtherConfiguration clientLwM2mSettings = clientProfile.getClientLwM2mSettings();
timeout = clientLwM2mSettings.getEdrxCycle(); if (PowerMode.E_DRX.equals(clientLwM2mSettings.getPowerMode())) {
timeout = clientLwM2mSettings.getEdrxCycle();
}
} }
} }
if (timeout == null || timeout == 0L) { if (timeout == null || timeout == 0L) {

14
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java

@ -200,7 +200,8 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
attributesToFetch.add(SOFTWARE_URL); attributesToFetch.add(SOFTWARE_URL);
} }
var clientSettings = clientContext.getProfile(client.getRegistration()).getClientLwM2mSettings(); var clientProfile = clientContext.getProfile(client.getRegistration());
var clientSettings = clientProfile != null ? clientProfile.getClientLwM2mSettings() : null;
if (clientSettings != null) { if (clientSettings != null) {
initFwStrategy(client, clientSettings); initFwStrategy(client, clientSettings);
initSwStrategy(client, clientSettings); initSwStrategy(client, clientSettings);
@ -532,7 +533,8 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
} else { } else {
strategy = info.getDeliveryMethod() == FirmwareDeliveryMethod.PULL.code ? LwM2MFirmwareUpdateStrategy.OBJ_5_TEMP_URL : LwM2MFirmwareUpdateStrategy.OBJ_5_BINARY; strategy = info.getDeliveryMethod() == FirmwareDeliveryMethod.PULL.code ? LwM2MFirmwareUpdateStrategy.OBJ_5_TEMP_URL : LwM2MFirmwareUpdateStrategy.OBJ_5_BINARY;
} }
Boolean useObject19ForOtaInfo = clientContext.getProfile(client.getRegistration()).getClientLwM2mSettings().getUseObject19ForOtaInfo(); var clientProfile = clientContext.getProfile(client.getRegistration());
Boolean useObject19ForOtaInfo = clientProfile != null ? clientProfile.getClientLwM2mSettings().getUseObject19ForOtaInfo() : null;
if (useObject19ForOtaInfo != null && useObject19ForOtaInfo){ if (useObject19ForOtaInfo != null && useObject19ForOtaInfo){
sendInfoToObject19ForOta(client, FW_INFO_19_INSTANCE_ID, response, otaPackageId); sendInfoToObject19ForOta(client, FW_INFO_19_INSTANCE_ID, response, otaPackageId);
} }
@ -558,7 +560,8 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
if (TransportProtos.ResponseStatus.SUCCESS.equals(response.getResponseStatus())) { if (TransportProtos.ResponseStatus.SUCCESS.equals(response.getResponseStatus())) {
UUID otaPackageId = new UUID(response.getOtaPackageIdMSB(), response.getOtaPackageIdLSB()); UUID otaPackageId = new UUID(response.getOtaPackageIdMSB(), response.getOtaPackageIdLSB());
LwM2MSoftwareUpdateStrategy strategy = info.getStrategy(); LwM2MSoftwareUpdateStrategy strategy = info.getStrategy();
Boolean useObject19ForOtaInfo = clientContext.getProfile(client.getRegistration()).getClientLwM2mSettings().getUseObject19ForOtaInfo(); var clientProfile = clientContext.getProfile(client.getRegistration());
Boolean useObject19ForOtaInfo = clientProfile != null ? clientProfile.getClientLwM2mSettings().getUseObject19ForOtaInfo() : null;
if (useObject19ForOtaInfo != null && useObject19ForOtaInfo){ if (useObject19ForOtaInfo != null && useObject19ForOtaInfo){
sendInfoToObject19ForOta(client, SW_INFO_19_INSTANCE_ID, response, otaPackageId); sendInfoToObject19ForOta(client, SW_INFO_19_INSTANCE_ID, response, otaPackageId);
} }
@ -647,8 +650,9 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
LwM2MClientSwOtaInfo info = otaInfoStore.getSw(endpoint); LwM2MClientSwOtaInfo info = otaInfoStore.getSw(endpoint);
if (info == null) { if (info == null) {
var profile = clientContext.getProfile(client.getRegistration()); var profile = clientContext.getProfile(client.getRegistration());
info = new LwM2MClientSwOtaInfo(endpoint, profile.getClientLwM2mSettings().getSwUpdateResource(), OtherConfiguration clientLwM2mSettings = profile == null ? new OtherConfiguration() : profile.getClientLwM2mSettings();
LwM2MSoftwareUpdateStrategy.fromStrategySwByCode(profile.getClientLwM2mSettings().getSwUpdateStrategy())); info = new LwM2MClientSwOtaInfo(endpoint, clientLwM2mSettings.getSwUpdateResource(),
LwM2MSoftwareUpdateStrategy.fromStrategySwByCode(clientLwM2mSettings.getSwUpdateStrategy()));
update(info); update(info);
} }
return info; return info;

69
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java

@ -420,7 +420,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
}); });
if (!clients.isEmpty()) { if (!clients.isEmpty()) {
var oldProfile = clientContext.getProfile(clients.get(0).getRegistration()); var oldProfile = clientContext.getProfile(clients.get(0).getRegistration());
this.onDeviceProfileUpdate(clients, oldProfile, deviceProfile); if (oldProfile != null) this.onDeviceProfileUpdate(clients, oldProfile, deviceProfile);
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}] failed to update profile: {} [{}]", deviceProfile.getId(), e.getMessage(), deviceProfile); log.warn("[{}] failed to update profile: {} [{}]", deviceProfile.getId(), e.getMessage(), deviceProfile);
@ -725,8 +725,10 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
private void onDeviceUpdate(LwM2mClient lwM2MClient, Device device, Optional<DeviceProfile> deviceProfileOpt) { private void onDeviceUpdate(LwM2mClient lwM2MClient, Device device, Optional<DeviceProfile> deviceProfileOpt) {
var oldProfile = clientContext.getProfile(lwM2MClient.getRegistration()); var oldProfile = clientContext.getProfile(lwM2MClient.getRegistration());
deviceProfileOpt.ifPresent(deviceProfile -> this.onDeviceProfileUpdate(Collections.singletonList(lwM2MClient), oldProfile, deviceProfile)); if (oldProfile != null) {
lwM2MClient.onDeviceUpdate(device, deviceProfileOpt); deviceProfileOpt.ifPresent(deviceProfile -> this.onDeviceProfileUpdate(Collections.singletonList(lwM2MClient), oldProfile, deviceProfile));
lwM2MClient.onDeviceUpdate(device, deviceProfileOpt);
}
} }
/** /**
@ -740,45 +742,12 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
Set<String> paths = updateResource.getPaths(); Set<String> paths = updateResource.getPaths();
ResultsAddKeyValueProto results = new ResultsAddKeyValueProto(); ResultsAddKeyValueProto results = new ResultsAddKeyValueProto();
var profile = clientContext.getProfile(registration); var profile = clientContext.getProfile(registration);
List<TransportProtos.KeyValueProto> resultAttributes = new ArrayList<>(); if (profile != null) {
Set<String> attributes = profile.getObserveAttr().getAttribute().stream()
.filter(paths::contains)
.collect(Collectors.toSet());
if (!attributes.isEmpty()){
attributes.stream()
.map(attr -> this.getKvToThingsBoard(attr, registration))
.filter(Objects::nonNull)
.forEach(resultAttributes::add);
}
List<TransportProtos.KeyValueProto> resultTelemetries = new ArrayList<>();
Set<String> telemetries = profile.getObserveAttr().getTelemetry().stream()
.filter(paths::contains)
.collect(Collectors.toSet());
if (!telemetries.isEmpty()){
telemetries.stream()
.map(telemetry -> this.getKvToThingsBoard(telemetry, registration))
.filter(Objects::nonNull)
.forEach(resultTelemetries::add);
}
if (resultAttributes.size() > 0) {
results.setResultAttributes(resultAttributes);
}
if (resultTelemetries.size() > 0) {
results.setResultTelemetries(resultTelemetries);
}
return results;
}
private ResultsAddKeyValueProto getParametersFromProfile(Registration registration, Set<String> path) {
if (!path.isEmpty()) {
ResultsAddKeyValueProto results = new ResultsAddKeyValueProto();
var profile = clientContext.getProfile(registration);
List<TransportProtos.KeyValueProto> resultAttributes = new ArrayList<>(); List<TransportProtos.KeyValueProto> resultAttributes = new ArrayList<>();
Set<String> attributes = profile.getObserveAttr().getAttribute().stream() Set<String> attributes = profile.getObserveAttr().getAttribute().stream()
.map(LwM2MTransportUtil::fromVersionedIdToObjectId) .filter(paths::contains)
.filter(path::contains)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
if (!attributes.isEmpty()){ if (!attributes.isEmpty()) {
attributes.stream() attributes.stream()
.map(attr -> this.getKvToThingsBoard(attr, registration)) .map(attr -> this.getKvToThingsBoard(attr, registration))
.filter(Objects::nonNull) .filter(Objects::nonNull)
@ -786,10 +755,9 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
} }
List<TransportProtos.KeyValueProto> resultTelemetries = new ArrayList<>(); List<TransportProtos.KeyValueProto> resultTelemetries = new ArrayList<>();
Set<String> telemetries = profile.getObserveAttr().getTelemetry().stream() Set<String> telemetries = profile.getObserveAttr().getTelemetry().stream()
.map(LwM2MTransportUtil::fromVersionedIdToObjectId) .filter(paths::contains)
.filter(path::contains)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
if (!telemetries.isEmpty()){ if (!telemetries.isEmpty()) {
telemetries.stream() telemetries.stream()
.map(telemetry -> this.getKvToThingsBoard(telemetry, registration)) .map(telemetry -> this.getKvToThingsBoard(telemetry, registration))
.filter(Objects::nonNull) .filter(Objects::nonNull)
@ -801,14 +769,15 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
if (resultTelemetries.size() > 0) { if (resultTelemetries.size() > 0) {
results.setResultTelemetries(resultTelemetries); results.setResultTelemetries(resultTelemetries);
} }
return results;
} }
return null; return results;
} }
private TransportProtos.KeyValueProto getKvToThingsBoard(String pathIdVer, Registration registration) { private TransportProtos.KeyValueProto getKvToThingsBoard(String pathIdVer, Registration registration) {
LwM2mClient lwM2MClient = this.clientContext.getClientByEndpoint(registration.getEndpoint()); LwM2mClient lwM2MClient = this.clientContext.getClientByEndpoint(registration.getEndpoint());
Map<String, String> names = clientContext.getProfile(lwM2MClient.getRegistration()).getObserveAttr().getKeyName(); var clientProfile = clientContext.getProfile(lwM2MClient.getRegistration());
if (clientProfile == null) return null;
Map<String, String> names = clientProfile.getObserveAttr().getKeyName();
if (names != null && names.containsKey(pathIdVer)) { if (names != null && names.containsKey(pathIdVer)) {
String resourceName = names.get(pathIdVer); String resourceName = names.get(pathIdVer);
if (resourceName != null && !resourceName.isEmpty()) { if (resourceName != null && !resourceName.isEmpty()) {
@ -899,10 +868,12 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
private void onDeviceProfileUpdate(List<LwM2mClient> clients, Lwm2mDeviceProfileTransportConfiguration oldProfileTransportConfiguration, DeviceProfile deviceProfile) { private void onDeviceProfileUpdate(List<LwM2mClient> clients, Lwm2mDeviceProfileTransportConfiguration oldProfileTransportConfiguration, DeviceProfile deviceProfile) {
if (clientContext.profileUpdate(deviceProfile) != null) { if (clientContext.profileUpdate(deviceProfile) != null) {
var newProfileTransportConfiguration = clientContext.getProfile(clients.get(0).getRegistration()); var newProfileTransportConfiguration = clientContext.getProfile(clients.get(0).getRegistration());
ParametersUpdateAnalyzeResult parametersUpdate = getParametersUpdate(oldProfileTransportConfiguration, newProfileTransportConfiguration); if (newProfileTransportConfiguration != null) {
ParametersObserveAnalyzeResult parametersObserve = getParametersObserve(oldProfileTransportConfiguration.getObserveAttr(), newProfileTransportConfiguration.getObserveAttr(), deviceProfile.getId().getId()); ParametersUpdateAnalyzeResult parametersUpdate = getParametersUpdate(oldProfileTransportConfiguration, newProfileTransportConfiguration);
compareAndSetWriteAttributesObservations(clients, parametersUpdate, parametersObserve); ParametersObserveAnalyzeResult parametersObserve = getParametersObserve(oldProfileTransportConfiguration.getObserveAttr(), newProfileTransportConfiguration.getObserveAttr(), deviceProfile.getId().getId());
updateValueOta(clients, newProfileTransportConfiguration, oldProfileTransportConfiguration); compareAndSetWriteAttributesObservations(clients, parametersUpdate, parametersObserve);
updateValueOta(clients, newProfileTransportConfiguration, oldProfileTransportConfiguration);
}
} }
} }

2
dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java

@ -85,7 +85,7 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
private String mqttsPemCertFile; private String mqttsPemCertFile;
@Value("${device.connectivity.coaps.pem_cert_file:}") @Value("${device.connectivity.coaps.pem_cert_file:}")
private String coapsPemCertFile; private String coapsPemCertFile;
@Value("${device.connectivity.gateway.image_version:3.7-stable}") @Value("${device.connectivity.gateway.image_version:3.8-stable}")
private String gatewayImageVersion; private String gatewayImageVersion;
@Override @Override

86
msa/pom.xml

@ -125,6 +125,92 @@
</plugins> </plugins>
</build> </build>
</profile> </profile>
<profile>
<id>push-lts-docker-amd-arm-images</id>
<activation>
<property>
<name>push-lts-docker-amd-arm-images</name>
</property>
</activation>
<properties>
<docker.lts.tag>4.3.0-latest</docker.lts.tag>
<docker.skip.latest.tag>false</docker.skip.latest.tag>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>push-latest-docker-amd-arm-images</id>
<phase>${docker.push-arm-amd-image.phase}</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<skip>${docker.skip.latest.tag}</skip>
<executable>docker</executable>
<workingDirectory>${project.build.directory}</workingDirectory>
<arguments>
<argument>buildx</argument>
<argument>build</argument>
<argument>-t</argument>
<argument>${docker.repo}/${docker.name}:latest</argument>
<argument>--platform=linux/amd64,linux/arm64</argument>
<argument>-o</argument>
<argument>type=registry</argument>
<argument>.</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>push-version-docker-amd-arm-images</id>
<phase>${docker.push-arm-amd-image.phase}</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>docker</executable>
<workingDirectory>${project.build.directory}</workingDirectory>
<arguments>
<argument>buildx</argument>
<argument>build</argument>
<argument>-t</argument>
<argument>${docker.repo}/${docker.name}:${project.version}</argument>
<argument>--platform=linux/amd64,linux/arm64</argument>
<argument>-o</argument>
<argument>type=registry</argument>
<argument>.</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>push-lts-docker-amd-arm-images</id>
<phase>${docker.push-arm-amd-image.phase}</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>docker</executable>
<workingDirectory>${project.build.directory}</workingDirectory>
<arguments>
<argument>buildx</argument>
<argument>build</argument>
<argument>-t</argument>
<argument>${docker.repo}/${docker.name}:${docker.lts.tag}</argument>
<argument>--platform=linux/amd64,linux/arm64</argument>
<argument>-o</argument>
<argument>type=registry</argument>
<argument>.</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles> </profiles>
<build> <build>
<extensions> <extensions>

112
ui-ngx/src/app/core/services/calculated-field-form.service.ts

@ -0,0 +1,112 @@
///
/// 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 { DestroyRef, inject, Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { pairwise, switchMap } from 'rxjs/operators';
import { Observable } from 'rxjs';
import {
CalculatedField,
CalculatedFieldConfiguration,
CalculatedFieldEventArguments,
CalculatedFieldType,
OutputStrategyType
} from '@shared/models/calculated-field.models';
import { oneSpaceInsideRegex } from '@shared/models/regex.constants';
import { isDefined } from '@core/utils';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { CalculatedFieldsTableEntity } from '@home/components/calculated-fields/calculated-fields-table-config';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Injectable({ providedIn: 'root' })
export class CalculatedFieldFormService {
private fb = inject(FormBuilder);
private calculatedFieldsService = inject(CalculatedFieldsService);
buildForm(): FormGroup {
return this.fb.group({
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
entityId: [null, Validators.required],
type: [CalculatedFieldType.SIMPLE],
debugSettings: [],
configuration: this.fb.control<CalculatedFieldConfiguration>({} as CalculatedFieldConfiguration),
});
}
buildAlarmRuleForm(): FormGroup {
return this.fb.group({
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
entityId: [null, Validators.required],
type: [CalculatedFieldType.ALARM],
debugSettings: [],
configuration: this.fb.group({
type: [CalculatedFieldType.ALARM],
arguments: this.fb.control({}, Validators.required),
propagate: [false],
propagateToOwner: [false],
propagateToTenant: [false],
propagateRelationTypes: [null],
createRules: [null, Validators.required],
clearRule: [null],
}),
});
}
setupTypeChange(form: FormGroup, destroyRef: DestroyRef, isEditActive?: () => boolean): void {
form.get('type').valueChanges.pipe(
pairwise(),
takeUntilDestroyed(destroyRef)
).subscribe(([prevType, nextType]) => {
const shouldCheck = isEditActive ? isEditActive() : true;
if (shouldCheck) {
if (![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(prevType) ||
![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(nextType)) {
form.get('configuration').setValue({} as CalculatedFieldConfiguration, { emitEvent: false });
}
}
});
}
prepareConfig(configuration: CalculatedFieldConfiguration): CalculatedFieldConfiguration {
const config = configuration || ({} as CalculatedFieldConfiguration);
if (config.type !== CalculatedFieldType.ALARM) {
if (isDefined(config?.output) && !config?.output?.strategy) {
config.output.strategy = { type: OutputStrategyType.RULE_CHAIN };
}
}
return config;
}
testScript(
calculatedFieldId: string,
formValue: CalculatedField,
testDialogFn: (calculatedField: CalculatedFieldsTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit?: boolean, expression?: string) => Observable<string>,
destroyRef: DestroyRef,
expression?: string,
): Observable<string> {
if (calculatedFieldId) {
return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true})
.pipe(
switchMap(event => {
const args = event?.arguments ? JSON.parse(event.arguments) : null;
return testDialogFn(formValue, args, false, expression);
}),
takeUntilDestroyed(destroyRef)
);
}
return testDialogFn(formValue, null, false, expression);
}
}

4
ui-ngx/src/app/core/services/utils.service.ts

@ -454,11 +454,11 @@ export class UtilsService {
return base64toObj(b64Encoded); return base64toObj(b64Encoded);
} }
public applyCssToElement(renderer: Renderer2, element: any, cssClassPrefix: string, css: string): string { public applyCssToElement(renderer: Renderer2, element: any, cssClassPrefix: string, css: string, addTbDefaultClass: boolean = false): string {
const cssParser = new cssjs(); const cssParser = new cssjs();
cssParser.testMode = false; cssParser.testMode = false;
const cssClass = `${cssClassPrefix}-${guid()}`; const cssClass = `${cssClassPrefix}-${guid()}`;
cssParser.cssPreviewNamespace = cssClass; cssParser.cssPreviewNamespace = addTbDefaultClass ? 'tb-default .' + cssClass : cssClass;
cssParser.createStyleElement(cssClass, css); cssParser.createStyleElement(cssClass, css);
renderer.addClass(element, cssClass); renderer.addClass(element, cssClass);
return cssClass; return cssClass;

34
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html

@ -58,31 +58,15 @@
/> />
</div> </div>
@if (!data.entityId) { @if (!data.entityId) {
<div class="flex flex-row gap-4 xs:flex-col" formGroupName="entityId"> <tb-entity-select formControlName="entityId"
<tb-entity-type-select #entityTypeSelect appearance="outline"
appearance="outline" required
subscriptSizing="dynamic" entityTypeLabel="{{ 'entity.entity-type' | translate }}"
class="flex-1" [filterAllowedEntityTypes]="false"
showLabel [allowedEntityTypes]="alarmRuleEntityTypeList"
required [defaultEntityType]="EntityType.DEVICE_PROFILE"
label="{{ 'entity.entity-type' | translate }}" (entityChanged)="changeEntity($event)">
[filterAllowedEntityTypes]="false" </tb-entity-select>
[allowedEntityTypes]="alarmRuleEntityTypeList"
formControlName="entityType">
</tb-entity-type-select>
@if (fieldFormGroup.get('entityId.entityType').value) {
<tb-entity-autocomplete #entityAutocompleteComponent
formControlName="id"
appearance="outline"
subscriptSizing="dynamic"
class="flex-1"
[placeholder]="'action.set' | translate"
[required]="true"
[entityType]="fieldFormGroup.get('entityId.entityType').value"
(entityChanged)="changeEntity($event)"
/>
}
</div>
} }
</div> </div>
<ng-container formGroupName="configuration"> <ng-container formGroupName="configuration">

63
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts

@ -14,16 +14,15 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, DestroyRef, Inject, ViewChild, ViewEncapsulation } from '@angular/core'; import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { CalculatedField, CalculatedFieldArgument, CalculatedFieldType } from '@shared/models/calculated-field.models'; import { CalculatedField, CalculatedFieldArgument, CalculatedFieldType } from '@shared/models/calculated-field.models';
import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from '@shared/models/rule-node.models'; import { ScriptLanguage } from '@shared/models/rule-node.models';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
@ -39,13 +38,11 @@ import {
} from "@shared/models/alarm-rule.models"; } from "@shared/models/alarm-rule.models";
import { deepTrim } from "@core/utils"; import { deepTrim } from "@core/utils";
import { combineLatest, Observable } from "rxjs"; import { combineLatest, Observable } from "rxjs";
import { debounceTime, startWith, switchMap } from "rxjs/operators"; import { debounceTime, startWith } from "rxjs/operators";
import { EntityTypeSelectComponent } from "@shared/components/entity/entity-type-select.component";
import { EntityAutocompleteComponent } from "@shared/components/entity/entity-autocomplete.component";
import { EntityService } from "@core/http/entity.service";
import { RelationTypes } from "@shared/models/relation.models"; import { RelationTypes } from "@shared/models/relation.models";
import { StringItemsOption } from "@shared/components/string-items-list.component"; import { StringItemsOption } from "@shared/components/string-items-list.component";
import { BaseData } from "@shared/models/base-data"; import { BaseData } from "@shared/models/base-data";
import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service';
export interface AlarmRuleDialogData { export interface AlarmRuleDialogData {
value?: CalculatedField; value?: CalculatedField;
@ -67,24 +64,7 @@ export interface AlarmRuleDialogData {
}) })
export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogComponent, CalculatedField> { export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogComponent, CalculatedField> {
fieldFormGroup = this.fb.group({ fieldFormGroup: FormGroup ;
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
type: [CalculatedFieldType.ALARM],
debugSettings: [],
entityId: this.fb.group({
entityType: this.fb.control<EntityType | AliasEntityType | null>(EntityType.DEVICE_PROFILE, Validators.required),
id: [null as null | string, Validators.required],
}),
configuration: this.fb.group({
arguments: this.fb.control({}, Validators.required),
propagate: [false],
propagateToOwner: [false],
propagateToTenant: [false],
propagateRelationTypes: [null],
createRules: [null, Validators.required],
clearRule: [null],
}),
});
additionalDebugActionConfig = this.data.value?.id ? { additionalDebugActionConfig = this.data.value?.id ? {
...this.data.additionalDebugActionConfig, ...this.data.additionalDebugActionConfig,
@ -105,18 +85,15 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
disabledArguments = false; disabledArguments = false;
isLoading = false; isLoading = false;
@ViewChild('entityTypeSelect') entityTypeSelect: EntityTypeSelectComponent;
@ViewChild('entityAutocompleteComponent') entityAutocompleteComponent: EntityAutocompleteComponent;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData, @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData,
protected dialogRef: MatDialogRef<AlarmRuleDialogComponent, CalculatedField>, protected dialogRef: MatDialogRef<AlarmRuleDialogComponent, CalculatedField>,
private calculatedFieldsService: CalculatedFieldsService, private calculatedFieldsService: CalculatedFieldsService,
private entityService: EntityService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private fb: FormBuilder) { private cfFormService: CalculatedFieldFormService) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.fieldFormGroup = this.cfFormService.buildAlarmRuleForm();
this.applyDialogData(); this.applyDialogData();
this.updateRulesValidators(); this.updateRulesValidators();
@ -132,7 +109,7 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
if (!this.data.entityId) { if (!this.data.entityId) {
combineLatest([ combineLatest([
this.fieldFormGroup.get('entityId.id')!.valueChanges.pipe(startWith(this.fieldFormGroup.get('entityId.id')!.value)), this.fieldFormGroup.get('entityId')!.valueChanges.pipe(startWith(this.fieldFormGroup.get('entityId')!.value)),
this.fieldFormGroup.get('name')!.valueChanges.pipe(startWith(this.fieldFormGroup.get('name')!.value)) this.fieldFormGroup.get('name')!.valueChanges.pipe(startWith(this.fieldFormGroup.get('name')!.value))
]).pipe( ]).pipe(
debounceTime(50), debounceTime(50),
@ -196,8 +173,6 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
}); });
} else { } else {
this.fieldFormGroup.get('name').markAsTouched(); this.fieldFormGroup.get('name').markAsTouched();
this.entityTypeSelect?.markAsTouched();
this.entityAutocompleteComponent?.markAsTouched();
} }
} }
@ -207,18 +182,13 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
} }
onTestScript(expression: string): Observable<string> { onTestScript(expression: string): Observable<string> {
const calculatedFieldId = this.data.value?.id?.id; return this.cfFormService.testScript(
if (calculatedFieldId) { this.data.value?.id?.id,
return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true}) this.fromGroupValue,
.pipe( this.data.getTestScriptDialogFn,
switchMap(event => { this.destroyRef,
const args = event?.arguments ? JSON.parse(event.arguments) : null; expression
return this.data.getTestScriptDialogFn(this.fromGroupValue, expression, args, false); );
}),
takeUntilDestroyed(this.destroyRef)
)
}
return this.data.getTestScriptDialogFn(this.fromGroupValue, expression, null, false);
} }
private updateRulesValidators(): void { private updateRulesValidators(): void {
@ -250,5 +220,4 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
changeEntity(entity: BaseData<EntityId>): void { changeEntity(entity: BaseData<EntityId>): void {
this.entityName = entity.name; this.entityName = entity.name;
} }
} }

1
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts

@ -196,6 +196,7 @@ export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAcces
cancel() { cancel() {
this.updateAlarmRuleConfigForm(this.alarmRuleFilterConfig); this.updateAlarmRuleConfigForm(this.alarmRuleFilterConfig);
this.alarmRuleFilterConfigForm.markAsPristine();
if (this.overlayRef) { if (this.overlayRef) {
this.overlayRef.dispose(); this.overlayRef.dispose();
} else { } else {

5
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts

@ -53,6 +53,7 @@ import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alar
import { import {
AlarmRuleFilterPredicateNoDataValueComponent AlarmRuleFilterPredicateNoDataValueComponent
} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component"; } from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component";
import { AlarmRulesComponent } from '@home/components/alarm-rules/alarm-rules.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -73,7 +74,8 @@ import {
AlarmRuleDetailsDialogComponent, AlarmRuleDetailsDialogComponent,
AlarmRuleFilterConfigComponent, AlarmRuleFilterConfigComponent,
AlarmRuleTableHeaderComponent, AlarmRuleTableHeaderComponent,
AlarmRuleFilterPredicateNoDataValueComponent AlarmRuleFilterPredicateNoDataValueComponent,
AlarmRulesComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -83,6 +85,7 @@ import {
], ],
exports: [ exports: [
AlarmRuleDialogComponent, AlarmRuleDialogComponent,
AlarmRulesComponent
] ]
}) })
export class AlarmRuleModule { } export class AlarmRuleModule { }

86
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts

@ -21,7 +21,7 @@ import {
EntityTableColumn, EntityTableColumn,
EntityTableConfig EntityTableConfig
} from '@home/models/entity/entities-table-config.models'; } from '@home/models/entity/entities-table-config.models';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
@ -36,7 +36,7 @@ import { DestroyRef, Renderer2 } from '@angular/core';
import { EntityDebugSettings } from '@shared/models/entity.models'; import { EntityDebugSettings } from '@shared/models/entity.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { catchError, filter, switchMap, tap } from 'rxjs/operators'; import { catchError, filter, first, switchMap, tap } from 'rxjs/operators';
import { import {
ArgumentEntityType, ArgumentEntityType,
ArgumentType, ArgumentType,
@ -67,6 +67,10 @@ import {
CalculatedFieldScriptTestDialogComponent, CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestScriptDialogData CalculatedFieldTestScriptDialogData
} from "@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component"; } from "@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component";
import { AlarmRulesTabsComponent } from '@home/pages/alarm/alarm-rules-tabs.component';
import { Router } from '@angular/router';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { AlarmRulesComponent } from '@home/components/alarm-rules/alarm-rules.component';
type AlarmRuleTableEntity = CalculatedField | CalculatedFieldInfo; type AlarmRuleTableEntity = CalculatedField | CalculatedFieldInfo;
@ -75,7 +79,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
readonly tenantId = getCurrentAuthUser(this.store).tenantId; readonly tenantId = getCurrentAuthUser(this.store).tenantId;
additionalDebugActionConfig = { additionalDebugActionConfig = {
title: this.translate.instant('action.see-debug-events'), title: this.translate.instant('action.see-debug-events'),
action: (calculatedField: AlarmRuleTableEntity) => this.openDebugEventsDialog.call(this, calculatedField), action: (calculatedField: AlarmRuleTableEntity) => this.openDebugEventsDialog.call(this, null, calculatedField),
}; };
alarmRuleFilterConfig: CalculatedFieldsQuery; alarmRuleFilterConfig: CalculatedFieldsQuery;
@ -93,27 +97,26 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
private importExportService: ImportExportService, private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService, private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService, private utilsService: UtilsService,
private router: Router,
public pageMode: boolean = false, public pageMode: boolean = false,
) { ) {
super(); super();
if (this.pageMode) { if (this.pageMode) {
this.headerComponent = AlarmRuleTableHeaderComponent; this.headerComponent = AlarmRuleTableHeaderComponent;
this.entityComponent = AlarmRulesComponent;
this.entityTabsComponent = AlarmRulesTabsComponent;
this.rowPointer = true; this.rowPointer = true;
this.handleRowClick = ($event, model) => {
this.editCalculatedField($event, model);
return true;
};
} }
this.tableTitle = this.pageMode ? '' : this.translate.instant('alarm-rule.alarm-rules'); this.tableTitle = this.pageMode ? '' : this.translate.instant('alarm-rule.alarm-rules');
this.detailsPanelEnabled = false; this.detailsPanelEnabled = this.pageMode;
this.entityResources = entityTypeResources.get(EntityType.CALCULATED_FIELD);
this.entityType = EntityType.CALCULATED_FIELD; this.entityType = EntityType.CALCULATED_FIELD;
this.entityTranslations = { this.entityTranslations = {
type: 'alarm-rule.alarm-rule', type: 'alarm-rule.alarm-rule',
typePlural: 'alarm-rule.alarm-rules', typePlural: 'alarm-rule.alarm-rules',
list: 'alarm-rule.list', list: 'alarm-rule.list',
add: 'action.add', add: 'action.add',
details: 'alarm-rule.details',
noEntities: 'alarm-rule.no-found', noEntities: 'alarm-rule.no-found',
search: 'action.search', search: 'action.search',
selectedEntities: 'alarm-rule.selected-fields' selectedEntities: 'alarm-rule.selected-fields'
@ -121,11 +124,17 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink); this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink);
this.addEntity = this.getCalculatedAlarmDialog.bind(this); this.addEntity = this.getCalculatedAlarmDialog.bind(this);
this.loadEntity = id => this.calculatedFieldsService.getCalculatedFieldById(id.id);
this.saveEntity = (alarmRule) => this.calculatedFieldsService.saveCalculatedField(alarmRule);
this.deleteEntityTitle = (field) => this.translate.instant('alarm-rule.delete-title', {title: field.name}); this.deleteEntityTitle = (field) => this.translate.instant('alarm-rule.delete-title', {title: field.name});
this.deleteEntityContent = () => this.translate.instant('alarm-rule.delete-text'); this.deleteEntityContent = () => this.translate.instant('alarm-rule.delete-text');
this.deleteEntitiesTitle = count => this.translate.instant('alarm-rule.delete-multiple-title', {count}); this.deleteEntitiesTitle = count => this.translate.instant('alarm-rule.delete-multiple-title', {count});
this.deleteEntitiesContent = () => this.translate.instant('alarm-rule.delete-multiple-text'); this.deleteEntitiesContent = () => this.translate.instant('alarm-rule.delete-multiple-text');
this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id); this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id);
this.onEntityAction = action => this.onCFAction(action);
this.addActionDescriptors = [ this.addActionDescriptors = [
{ {
name: this.translate.instant('alarm-rule.create'), name: this.translate.instant('alarm-rule.create'),
@ -177,7 +186,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
name: this.translate.instant('entity-view.events'), name: this.translate.instant('entity-view.events'),
icon: 'mdi:clipboard-text-clock', icon: 'mdi:clipboard-text-clock',
isEnabled: () => true, isEnabled: () => true,
onAction: ($event, entity) => this.openDebugEventsDialog($event, entity), onAction: ($event, entity) =>
this.pageMode ? this.openDebugTab($event, entity) : this.openDebugEventsDialog($event, entity),
}, },
{ {
name: '', name: '',
@ -186,14 +196,18 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
isEnabled: () => true, isEnabled: () => true,
iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline',
onAction: ($event, entity) => this.onOpenDebugConfig($event, entity), onAction: ($event, entity) => this.onOpenDebugConfig($event, entity),
},
{
name: this.translate.instant('action.edit'),
icon: 'edit',
isEnabled: () => true,
onAction: ($event, entity) => this.editCalculatedField($event, entity),
} }
); );
if (!this.pageMode) {
this.cellActionDescriptors.push(
{
name: this.translate.instant('action.edit'),
icon: 'edit',
isEnabled: () => true,
onAction: ($event, entity) => this.editCalculatedField($event, entity),
}
)
}
} }
fetchCalculatedFields(pageLink: PageLink): Observable<PageData<AlarmRuleTableEntity>> { fetchCalculatedFields(pageLink: PageLink): Observable<PageData<AlarmRuleTableEntity>> {
@ -288,13 +302,29 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
debugEventTypes:[DebugEventType.DEBUG_CALCULATED_FIELD], debugEventTypes:[DebugEventType.DEBUG_CALCULATED_FIELD],
disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS], disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS],
defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD, defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD,
debugActionEnabledFn: () => false debugActionDisabled: true
} }
}) })
.afterClosed() .afterClosed()
.subscribe(); .subscribe();
} }
private openDebugTab($event: Event, calculatedField: AlarmRuleTableEntity) {
const table = this.getTable();
if (!table.isDetailsOpen) {
table.toggleEntityDetails($event, calculatedField);
if (table.entityDetailsPanel.matTabGroup._tabs.length > 1) {
table.entityDetailsPanel.matTabGroup.selectedIndex = 1;
} else {
table.entityDetailsPanel.matTabGroup._tabs.changes.pipe(
first()
).subscribe(() => {
table.entityDetailsPanel.matTabGroup.selectedIndex = 1;
})
}
}
}
private exportAlarmRule($event: Event, calculatedField: AlarmRuleTableEntity): void { private exportAlarmRule($event: Event, calculatedField: AlarmRuleTableEntity): void {
$event?.stopPropagation(); $event?.stopPropagation();
this.importExportService.exportCalculatedField(calculatedField.id.id); this.importExportService.exportCalculatedField(calculatedField.id.id);
@ -348,7 +378,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
).subscribe(() => this.updateData()); ).subscribe(() => this.updateData());
} }
private getTestScriptDialog(calculatedField: AlarmRuleTableEntity, expression: string, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true): Observable<string> { private getTestScriptDialog(calculatedField: AlarmRuleTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable<string> {
if (calculatedField.type === CalculatedFieldType.ALARM) { if (calculatedField.type === CalculatedFieldType.ALARM) {
const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
const type = calculatedField.configuration.arguments[key].refEntityKey.type; const type = calculatedField.configuration.arguments[key].refEntityKey.type;
@ -384,4 +414,22 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
return of(null); return of(null);
} }
} }
private openCalculatedField($event: Event, entity: AlarmRuleTableEntity) {
$event?.stopPropagation();
const url = this.router.createUrlTree(['alarms', 'alarm-rules', entity.id.id]);
this.router.navigateByUrl(url);
}
private onCFAction(action: EntityAction<AlarmRuleTableEntity>): boolean {
switch (action.action) {
case 'open':
this.openCalculatedField(action.event, action.entity);
return true;
case 'export':
this.exportAlarmRule(action.event, action.entity);
return true;
}
return false;
}
} }

4
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts

@ -36,7 +36,7 @@ import { EntityDebugSettingsService } from '@home/components/entity/debug/entity
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config"; import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config";
import { UtilsService } from "@core/services/utils.service"; import { UtilsService } from "@core/services/utils.service";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
@Component({ @Component({
selector: 'tb-alarm-rules-table', selector: 'tb-alarm-rules-table',
@ -70,6 +70,7 @@ export class AlarmRulesTableComponent {
private utilsService: UtilsService, private utilsService: UtilsService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router
) { ) {
this.pageMode = !!this.route.snapshot.data.isPage; this.pageMode = !!this.route.snapshot.data.isPage;
effect(() => { effect(() => {
@ -88,6 +89,7 @@ export class AlarmRulesTableComponent {
this.importExportService, this.importExportService,
this.entityDebugSettingsService, this.entityDebugSettingsService,
this.utilsService, this.utilsService,
this.router,
this.pageMode, this.pageMode,
); );
this.cd.markForCheck(); this.cd.markForCheck();

159
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html

@ -0,0 +1,159 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-details-buttons xs:flex xs:flex-col" *ngIf="!standalone">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[class.!hidden]="isEdit || isDetailsPage">
{{ 'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')"
[class.!hidden]="isEdit">
{{ 'action.export' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[class.!hidden]="hideDelete() || isEdit">
{{ 'action.delete' | translate }}
</button>
</div>
<div class="mat-padding flex flex-1 flex-col" [formGroup]="entityForm">
<div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel no-gap">
<div class="tb-form-panel-title mb-4">{{ 'common.general' | translate }}</div>
<div class="flex items-center gap-2">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'entity-field.title' | translate }}</mat-label>
<input matInput maxlength="255" formControlName="name" required>
@if (entityForm.get('name').errors && entityForm.get('name').touched) {
<mat-error>
@if (entityForm.get('name').hasError('required')) {
{{ 'common.hint.title-required' | translate }}
} @else if (entityForm.get('name').hasError('pattern')) {
{{ 'common.hint.title-pattern' | translate }}
} @else if (entityForm.get('name').hasError('maxlength')) {
{{ 'common.hint.title-max-length' | translate }}
}
</mat-error>
}
<mat-hint></mat-hint>
</mat-form-field>
<tb-entity-debug-settings-button
formControlName="debugSettings"
style="margin-bottom: 22px"
[entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig"
/>
</div>
<tb-entity-select formControlName="entityId"
appearance="outline"
required
entityTypeLabel="{{ 'entity.entity-type' | translate }}"
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="calculatedFieldsEntityTypeList"
(entityChanged)="changeEntity($event)">
</tb-entity-select>
</div>
<ng-container formGroupName="configuration">
<div class="tb-form-panel" [class.disabled]="!isEditValue">
<div class="tb-form-panel-title tb-required">{{ 'calculated-fields.arguments' | translate }}</div>
<tb-calculated-field-arguments-table formControlName="arguments"
[entityId]="entityId"
[tenantId]="tenantId"
[ownerId]="ownerId"
[watchKeyChange]="true"
[entityName]="entityName"/>
</div>
<div class="tb-form-panel" [class.disabled]="!isEditValue">
<div class="tb-form-panel-title">{{ 'alarm-rule.create-conditions' | translate }}</div>
<div class="flex flex-1 flex-col">
<tb-create-cf-alarm-rules formControlName="createRules" [arguments]="arguments" [testScript]="onTestScript.bind(this)">
</tb-create-cf-alarm-rules>
</div>
</div>
<div class="tb-form-panel" [class.disabled]="!isEditValue">
<div class="tb-form-panel-title">{{ 'alarm-rule.clear-condition' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-2 pb-2"
[class.!hidden]="!configFormGroup.get('clearRule').value">
<div class="clear-alarm-rule flex flex-1 flex-row">
<tb-cf-alarm-rule formControlName="clearRule" class="flex-1" [arguments]="arguments" [testScript]="onTestScript.bind(this)" isClearCondition>
</tb-cf-alarm-rule>
</div>
<button mat-icon-button
class="button-icon"
type="button"
(click)="removeClearAlarmRule()"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
<div *ngIf="!configFormGroup.get('clearRule').value">
<span translate class="tb-prompt flex items-center justify-center text-base">alarm-rule.no-clear-alarm-rule</span>
</div>
<div [class.!hidden]="configFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary"
[disabled]="!isEditValue"
type="button"
(click)="addClearAlarmRule()">
{{ 'alarm-rule.add-clear-alarm-rule' | translate }}
</button>
</div>
</div>
<div class="tb-form-panel no-gap">
<mat-expansion-panel class="tb-settings" [expanded]="false">
<mat-expansion-panel-header [class.tb-disabled]="!isEditValue">
{{ 'alarm-rule.advanced-settings' | translate }}
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-panel stroked no-padding no-gap">
<div class="tb-form-row no-border">
<mat-slide-toggle class="mat-slide margin" formControlName="propagate">
{{ 'alarm-rule.propagate-alarm' | translate }}
</mat-slide-toggle>
</div>
@if (configFormGroup.get('propagate').value) {
<tb-string-items-list appearance="outline"
class="flex flex-1 px-4"
label="{{ 'alarm-rule.alarm-rule-relation-types-list' | translate }}"
placeholder="{{ 'alarm-rule.alarm-rule-relation-types-list' | translate }}"
hint="{{ 'alarm-rule.alarm-rule-relation-types-list-hint' | translate }}"
[predefinedValues]="predefinedTypeValues"
formControlName="propagateRelationTypes">
</tb-string-items-list>
}
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide margin" formControlName="propagateToOwner">
{{ 'alarm-rule.propagate-alarm-to-owner' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide margin" formControlName="propagateToTenant">
{{ 'alarm-rule.propagate-alarm-to-tenant' | translate }}
</mat-slide-toggle>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</ng-container>
</div>
</div>

175
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts

@ -0,0 +1,175 @@
///
/// 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, inject, Inject, Input } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityComponent } from '@home/components/entity/entity.component';
import { FormBuilder, FormGroup } from '@angular/forms';
import { EntityType } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core';
import {
CalculatedFieldArgument,
CalculatedFieldConfiguration,
CalculatedFieldInfo,
calculatedFieldsEntityTypeList,
CalculatedFieldType
} from '@shared/models/calculated-field.models';
import { EntityId } from '@shared/models/id/entity-id';
import { BaseData } from '@shared/models/base-data';
import { Observable } from 'rxjs';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import {
CalculatedFieldsTableConfig,
CalculatedFieldsTableEntity
} from '@home/components/calculated-fields/calculated-fields-table-config';
import { TenantId } from '@shared/models/id/tenant-id';
import { StringItemsOption } from '@shared/components/string-items-list.component';
import { RelationTypes } from '@shared/models/relation.models';
import { AlarmRule, AlarmRuleConditionType, AlarmRuleExpressionType } from '@shared/models/alarm-rule.models';
import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service';
@Component({
selector: 'tb-alarm-rules',
templateUrl: './alarm-rules.component.html',
styleUrls: []
})
export class AlarmRulesComponent extends EntityComponent<CalculatedFieldsTableEntity> {
@Input()
standalone = false;
@Input()
entityName: string;
readonly ownerId = new TenantId(getCurrentAuthUser(this.store).tenantId);
readonly tenantId = getCurrentAuthUser(this.store).tenantId;
readonly EntityType = EntityType;
readonly calculatedFieldsEntityTypeList = calculatedFieldsEntityTypeList;
readonly CalculatedFieldType = CalculatedFieldType;
private cfFormService = inject(CalculatedFieldFormService);
private destroyRef = inject(DestroyRef);
constructor(protected store: Store<AppState>,
protected translate: TranslateService,
@Inject('entity') protected entityValue: CalculatedFieldInfo,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: CalculatedFieldsTableConfig,
protected fb: FormBuilder,
protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
}
hideDelete() {
if (this.entitiesTableConfig) {
return !this.entitiesTableConfig.deleteEnabled(this.entity);
} else {
return false;
}
}
additionalDebugActionConfig = {
...this.entitiesTableConfig.additionalDebugActionConfig,
action: () => this.entitiesTableConfig.additionalDebugActionConfig.action(
{ id: this.entity.id, ...this.entityFormValue() }, false,
(expression) => {
if (expression) {
this.entityForm.get('configuration').setValue({...this.entityFormValue().configuration, expression});
this.entityForm.get('configuration').markAsDirty();
}
}),
};
get entityId(): EntityId {
return this.entityForm.get('entityId').value;
}
get entitiesTableConfig(): CalculatedFieldsTableConfig {
return this.entitiesTableConfigValue;
}
changeEntity(entity: BaseData<EntityId>): void {
this.entityName = entity?.name;
}
buildForm(_entity?: CalculatedFieldInfo): FormGroup {
return inject(CalculatedFieldFormService).buildAlarmRuleForm();
}
updateForm(entity: CalculatedFieldInfo) {
const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.entityId, ...value } = entity ?? {};
setTimeout(() => {
this.entityForm.patchValue({ configuration, debugSettings, entityId, ...value }, {emitEvent: false});
});
if (!entityId) {
this.entityForm.get('configuration').disable({emitEvent: false});
}
}
onTestScript(expression?: string): Observable<string> {
return this.cfFormService.testScript(
this.entity?.id?.id,
this.entityFormValue(),
this.entitiesTableConfig.getTestScriptDialog.bind(this.entitiesTableConfig),
this.destroyRef,
expression
);
}
updateFormState() {
if (this.entityForm) {
if (this.isEditValue) {
this.entityForm.enable({emitEvent: false});
this.entityForm.get('entityId').disable({emitEvent: false});
} else {
this.entityForm.disable({emitEvent: false});
}
}
}
get arguments(): Record<string, CalculatedFieldArgument> {
return this.entityForm.get('configuration.arguments').value;
}
get predefinedTypeValues(): StringItemsOption[] {
return RelationTypes.map(type => ({
name: type,
value: type
}));
}
get configFormGroup(): FormGroup {
return this.entityForm.get('configuration') as FormGroup;
}
public removeClearAlarmRule() {
this.configFormGroup.patchValue({clearRule: null});
this.entityForm.markAsDirty();
}
public addClearAlarmRule() {
const clearAlarmRule: AlarmRule = {
condition: {
type: AlarmRuleConditionType.SIMPLE,
expression: {
type: AlarmRuleExpressionType.SIMPLE
}
}
};
this.configFormGroup.patchValue({clearRule: clearAlarmRule});
}
}

4
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-alarm-rule-condition flex flex-col gap-4 min-w-0" [formGroup]="alarmRuleConditionFormGroup"> <div class="tb-alarm-rule-condition flex min-w-0 flex-col gap-4" [formGroup]="alarmRuleConditionFormGroup">
<div class="tb-form-row column-xs"> <div class="tb-form-row column-xs">
<div class="min-w-40 xs:min-w-fit">{{ 'alarm-rule.condition' | translate }}</div> <div class="min-w-40 xs:min-w-fit">{{ 'alarm-rule.condition' | translate }}</div>
<button [disabled]="disabled" <button [disabled]="disabled"
@ -23,7 +23,7 @@
class="tb-alarm-rule-condition-button" class="tb-alarm-rule-condition-button"
mat-stroked-button [color]="conditionSet() && this.filtersArgumentsValid ? 'primary' : 'warn'" mat-stroked-button [color]="conditionSet() && this.filtersArgumentsValid ? 'primary' : 'warn'"
(click)="openFilterDialog($event)"> (click)="openFilterDialog($event)">
<div class="flex items-center gap-2 justify-between"> <div class="flex items-center justify-between gap-2">
<tb-alarm-rule-filter-text [alarmRuleExpression]="alarmRuleConditionFormGroup.get('expression').value" <tb-alarm-rule-filter-text [alarmRuleExpression]="alarmRuleConditionFormGroup.get('expression').value"
[arguments]="arguments" [arguments]="arguments"
[disabled]="disabled" [disabled]="disabled"

16
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts

@ -137,15 +137,19 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
writeValue(value: AlarmRuleCondition): void { writeValue(value: AlarmRuleCondition): void {
this.modelValue = value; this.modelValue = value;
this.updateConditionInfo(); this.updateConditionInfo();
if (value) { if (value && !this.disabled) {
this.onValidatorChange(); if (this.validate(this.alarmRuleConditionFormGroup)) {
this.onValidatorChange();
}
} }
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (changes.arguments) { if (changes.arguments) {
if (changes.arguments && !changes.arguments.firstChange && this.modelValue) { if (changes.arguments && !changes.arguments.firstChange && this.modelValue && !this.disabled) {
this.onValidatorChange(); if (this.validate(this.alarmRuleConditionFormGroup)) {
this.onValidatorChange();
}
} }
} }
} }
@ -181,7 +185,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, this.arguments); this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, this.arguments);
this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments)); this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments));
this.onValidatorChange = () => { this.onValidatorChange = () => {
control.updateValueAndValidity({ emitEvent: true }); control.updateValueAndValidity({ emitEvent: !this.disabled });
}; };
return this.conditionSet() && this.filtersArgumentsValid && this.schedulerArgumentsValid ? null : { return this.conditionSet() && this.filtersArgumentsValid && this.schedulerArgumentsValid ? null : {
alarmRuleCondition: { alarmRuleCondition: {
@ -303,7 +307,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
} }
private updateScheduleText() { private updateScheduleText() {
let schedule = this.modelValue?.schedule; const schedule = this.modelValue?.schedule;
this.scheduleText = ''; this.scheduleText = '';
if (isDefinedAndNotNull(schedule)) { if (isDefinedAndNotNull(schedule)) {
if (schedule.dynamicValueArgument) { if (schedule.dynamicValueArgument) {

2
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-form-panel no-border no-padding" [formGroup]="alarmRuleFormGroup"> <div class="tb-form-panel no-border no-padding" [class.disabled]="disabled" [formGroup]="alarmRuleFormGroup">
<tb-cf-alarm-rule-condition formControlName="condition" [arguments]="arguments" [required]="required" [testScript]="testScript" [isClearCondition]="isClearCondition"> <tb-cf-alarm-rule-condition formControlName="condition" [arguments]="arguments" [required]="required" [testScript]="testScript" [isClearCondition]="isClearCondition">
</tb-cf-alarm-rule-condition> </tb-cf-alarm-rule-condition>
<div class="tb-form-row space-between column-xs"> <div class="tb-form-row space-between column-xs">

11
ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts

@ -27,10 +27,9 @@ import {
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models'; import { AlarmSeverity, alarmSeverityColors, alarmSeverityTranslations } from '@shared/models/alarm.models';
import { AlarmRule } from "@shared/models/alarm-rule.models"; import { AlarmRule } from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { AlarmSeverityNotificationColors } from "@shared/models/notification.models";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { coerceBoolean } from "@shared/decorators/coercion"; import { coerceBoolean } from "@shared/decorators/coercion";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
@ -68,7 +67,7 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida
alarmSeverityEnum = AlarmSeverity; alarmSeverityEnum = AlarmSeverity;
alarmSeverityTranslationMap = alarmSeverityTranslations; alarmSeverityTranslationMap = alarmSeverityTranslations;
AlarmSeverityNotificationColors = AlarmSeverityNotificationColors; AlarmSeverityNotificationColors = alarmSeverityColors;
createAlarmRulesFormGroup = this.fb.group({ createAlarmRulesFormGroup = this.fb.group({
createAlarmRules: this.fb.array<{severity: AlarmSeverity, alarmRule: AlarmRule}>([]) createAlarmRules: this.fb.array<{severity: AlarmSeverity, alarmRule: AlarmRule}>([])
@ -115,13 +114,13 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida
} }
createAlarmRulesControls.push(this.fb.group({ createAlarmRulesControls.push(this.fb.group({
severity: [severity, Validators.required], severity: [severity, Validators.required],
alarmRule: [createAlarmRule, Validators.required] alarmRule: [{value: createAlarmRule, disabled: this.disabled}, Validators.required]
})); }));
}); });
} }
const formArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray; const formArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray;
formArray.clear(); formArray.clear({emitEvent: false});
createAlarmRulesControls.forEach(c => formArray.push(c)); createAlarmRulesControls.forEach(c => formArray.push(c, {emitEvent: false}));
if (this.disabled) { if (this.disabled) {
this.createAlarmRulesFormGroup.disable({emitEvent: false}); this.createAlarmRulesFormGroup.disable({emitEvent: false});
} else { } else {

19
ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts

@ -32,13 +32,15 @@ import {
AlarmCommentInfo, AlarmCommentInfo,
AlarmCommentType, AlarmCommentType,
AlarmMessage, AlarmMessage,
AlarmSeverity,
alarmSeverityTranslations,
getUserDisplayName getUserDisplayName
} from '@shared/models/alarm.models'; } from '@shared/models/alarm.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { ImportExportService } from '@shared/import-export/import-export.service'; import { ImportExportService } from '@shared/import-export/import-export.service';
import { isNotEmptyStr } from '@core/utils'; import { deepClone, isNotEmptyStr } from '@core/utils';
interface AlarmCommentsDisplayData { interface AlarmCommentsDisplayData {
commentId?: string; commentId?: string;
@ -153,8 +155,21 @@ export class AlarmCommentComponent implements OnInit {
private parseSystemComment(alarm: AlarmCommentInfo): string { private parseSystemComment(alarm: AlarmCommentInfo): string {
const subTypeKey = alarm.comment?.subtype; const subTypeKey = alarm.comment?.subtype;
if (subTypeKey && AlarmMessage[subTypeKey]) { if (subTypeKey && AlarmMessage[subTypeKey]) {
const alarmComment = deepClone(alarm.comment);
const translationKey = AlarmMessage[subTypeKey]; const translationKey = AlarmMessage[subTypeKey];
return this.translate.instant(translationKey, alarm.comment); if (alarmComment?.newSeverity) {
alarmComment.newSeverity =
(alarmSeverityTranslations.has(alarmComment.newSeverity)
? this.translate.instant(alarmSeverityTranslations.get(alarmComment.newSeverity))
: alarmComment.newSeverity) as AlarmSeverity;
}
if (alarmComment?.oldSeverity) {
alarmComment.oldSeverity =
(alarmSeverityTranslations.has(alarmComment.oldSeverity)
? this.translate.instant(alarmSeverityTranslations.get(alarmComment.oldSeverity))
: alarmComment.oldSeverity) as AlarmSeverity;
}
return this.translate.instant(translationKey, alarmComment);
} }
return alarm.comment.text; return alarm.comment.text;
} }

1
ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts

@ -224,6 +224,7 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal
cancel() { cancel() {
this.updateAlarmConfigForm(this.alarmFilterConfig); this.updateAlarmConfigForm(this.alarmFilterConfig);
this.alarmFilterConfigForm.markAsPristine();
if (this.overlayRef) { if (this.overlayRef) {
this.overlayRef.dispose(); this.overlayRef.dispose();
} else { } else {

2
ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts

@ -163,11 +163,13 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor {
cancel() { cancel() {
this.updateAuditLogFilterForm(this.auditLogFilter); this.updateAuditLogFilterForm(this.auditLogFilter);
this.auditLogFilterForm.markAsPristine();
this.auditLogOverlayRef.dispose(); this.auditLogOverlayRef.dispose();
} }
update() { update() {
this.auditLogFilterUpdated(this.auditLogFilterForm.value); this.auditLogFilterUpdated(this.auditLogFilterForm.value);
this.auditLogFilterForm.markAsPristine();
this.auditLogOverlayRef.dispose(); this.auditLogOverlayRef.dispose();
} }

136
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html

@ -0,0 +1,136 @@
<!--
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-details-buttons xs:flex xs:flex-col" *ngIf="!standalone">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[class.!hidden]="isEdit || isDetailsPage">
{{ 'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')"
[class.!hidden]="isEdit">
{{ 'action.export' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[class.!hidden]="hideDelete() || isEdit">
{{ 'action.delete' | translate }}
</button>
</div>
<div class="mat-padding flex flex-1 flex-col" [formGroup]="entityForm">
<div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel no-gap">
<div class="tb-form-panel-title mb-4">{{ 'common.general' | translate }}</div>
<div class="flex items-center gap-2">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'entity-field.title' | translate }}</mat-label>
<input matInput maxlength="255" formControlName="name" required>
@if (entityForm.get('name').errors && entityForm.get('name').touched) {
<mat-error>
@if (entityForm.get('name').hasError('required')) {
{{ 'common.hint.title-required' | translate }}
} @else if (entityForm.get('name').hasError('pattern')) {
{{ 'common.hint.title-pattern' | translate }}
} @else if (entityForm.get('name').hasError('maxlength')) {
{{ 'common.hint.title-max-length' | translate }}
}
</mat-error>
}
<mat-hint></mat-hint>
</mat-form-field>
<tb-entity-debug-settings-button
formControlName="debugSettings"
style="margin-bottom: 22px"
[entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig"
/>
</div>
<tb-entity-select formControlName="entityId"
appearance="outline"
required
entityTypeLabel="{{ 'entity.entity-type' | translate }}"
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="calculatedFieldsEntityTypeList"
(entityChanged)="changeEntity($event)">
</tb-entity-select>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'common.type' | translate }}</mat-label>
<mat-select formControlName="type">
@for (type of fieldTypes; track type) {
<mat-option [value]="type">{{ CalculatedFieldTypeTranslations.get(type).name | translate }}</mat-option>
}
</mat-select>
@if (CalculatedFieldTypeTranslations.get(entityForm.get('type').value).hint) {
<mat-hint>{{ CalculatedFieldTypeTranslations.get(entityForm.get('type').value).hint | translate }}</mat-hint>
}
</mat-form-field>
</div>
@switch (entityForm.get('type').value) {
@case (CalculatedFieldType.GEOFENCING) {
<tb-geofencing-configuration formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[ownerId]="ownerId"
[tenantId]="tenantId"
></tb-geofencing-configuration>
}
@case (CalculatedFieldType.PROPAGATION) {
<tb-propagation-configuration formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[tenantId]="tenantId"
[ownerId]="ownerId"
[testScript]="onTestScript.bind(this)"
></tb-propagation-configuration>
}
@case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) {
<tb-related-entities-aggregation-component formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[tenantId]="tenantId"
[testScript]="onTestScript.bind(this)"
></tb-related-entities-aggregation-component>
}
@case (CalculatedFieldType.ENTITY_AGGREGATION) {
<tb-entity-aggregation-component formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[tenantId]="tenantId"
></tb-entity-aggregation-component>
}
@default {
<tb-simple-configuration formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[ownerId]="ownerId"
[tenantId]="tenantId"
[isScript]="entityForm.get('type').value === CalculatedFieldType.SCRIPT"
[testScript]="onTestScript.bind(this)"
></tb-simple-configuration>
}
}
</div>
</div>

52
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss

@ -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.
*/
:host ::ng-deep {
.tbel-script-lang-chip {
line-height: 20px;
font-size: 14px;
font-weight: 500;
color: white;
border-radius: 100px;
width: 70px;
min-width: 70px;
display: flex;
justify-content: center;
margin-top: 2px;
margin-right: 4px;
}
.tb-js-func {
.ace_tb {
&.ace_calculated-field {
&-ctx {
color: #C52F00;
}
&-args {
color: #185F2A;
}
&-key {
color: #c24c1a;
}
&-time-window, &-values, &-func, &-value, &-ts, &-latestTs {
color: #7214D0;
}
&-start-ts, &-end-ts {
color: #2CAA00;
}
}
}
}
}

145
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts

@ -0,0 +1,145 @@
///
/// 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, inject, Inject, Input } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityComponent } from '../../components/entity/entity.component';
import { FormBuilder, FormGroup } from '@angular/forms';
import { EntityType } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core';
import {
CalculatedFieldConfiguration,
CalculatedFieldInfo,
calculatedFieldsEntityTypeList,
CalculatedFieldType,
calculatedFieldTypes,
CalculatedFieldTypeTranslations
} from '@shared/models/calculated-field.models';
import { EntityId } from '@shared/models/id/entity-id';
import { BaseData } from '@shared/models/base-data';
import { Observable } from 'rxjs';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import {
CalculatedFieldsTableConfig,
CalculatedFieldsTableEntity
} from '@home/components/calculated-fields/calculated-fields-table-config';
import { TenantId } from '@shared/models/id/tenant-id';
import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service';
@Component({
selector: 'tb-calculated-field',
templateUrl: './calculated-field.component.html',
styleUrls: ['./calculated-field.component.scss']
})
export class CalculatedFieldComponent extends EntityComponent<CalculatedFieldsTableEntity> {
@Input()
standalone = false;
@Input()
entityName: string;
disabledConfiguration = false;
readonly ownerId = new TenantId(getCurrentAuthUser(this.store).tenantId);
readonly tenantId = getCurrentAuthUser(this.store).tenantId;
readonly EntityType = EntityType;
readonly calculatedFieldsEntityTypeList = calculatedFieldsEntityTypeList;
readonly CalculatedFieldType = CalculatedFieldType;
readonly fieldTypes = calculatedFieldTypes;
readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations;
private cfFormService = inject(CalculatedFieldFormService);
private destroyRef = inject(DestroyRef);
constructor(protected store: Store<AppState>,
protected translate: TranslateService,
@Inject('entity') protected entityValue: CalculatedFieldInfo,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: CalculatedFieldsTableConfig,
protected fb: FormBuilder,
protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
}
hideDelete() {
if (this.entitiesTableConfig) {
return !this.entitiesTableConfig.deleteEnabled(this.entity);
} else {
return false;
}
}
additionalDebugActionConfig = {
...this.entitiesTableConfig.additionalDebugActionConfig,
action: () => this.entitiesTableConfig.additionalDebugActionConfig.action(
{ id: this.entity.id, ...this.entityFormValue() }, false,
(expression) => {
if (expression) {
this.entityForm.get('configuration').setValue({...this.entityFormValue().configuration, expression});
this.entityForm.get('configuration').markAsDirty();
}
}),
};
get entityId(): EntityId {
return this.entityForm.get('entityId').value;
}
get entitiesTableConfig(): CalculatedFieldsTableConfig {
return this.entitiesTableConfigValue;
}
changeEntity(entity: BaseData<EntityId>): void {
this.entityName = entity?.name;
}
buildForm(_entity?: CalculatedFieldInfo): FormGroup {
const form = inject(CalculatedFieldFormService).buildForm();
inject(CalculatedFieldFormService).setupTypeChange(form, inject(DestroyRef), () => this.isEditValue);
return form;
}
updateForm(entity: CalculatedFieldInfo) {
const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.entityId, ...value } = entity ?? {};
const preparedConfig = this.cfFormService.prepareConfig(configuration);
this.entityForm.patchValue({ type }, {emitEvent: false, onlySelf: true});
setTimeout(() => {
this.entityForm.patchValue({ configuration: preparedConfig, debugSettings, entityId, ...value }, {emitEvent: false});
});
}
onTestScript(expression?: string): Observable<string> {
return this.cfFormService.testScript(
this.entity?.id?.id,
this.entityFormValue(),
this.entitiesTableConfig.getTestScriptDialog.bind(this.entitiesTableConfig),
this.destroyRef,
expression
);
}
updateFormState() {
if (this.entityForm) {
if (this.isEditValue) {
this.entityForm.enable({emitEvent: false});
this.entityForm.get('entityId').disable({emitEvent: false});
} else {
this.entityForm.disable({emitEvent: false});
}
}
}
}

5
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts

@ -50,6 +50,7 @@ import {
import { import {
CalculatedFieldsFilterConfigComponent CalculatedFieldsFilterConfigComponent
} from '@home/components/calculated-fields/table-header/calculated-fields-filter-config.component'; } from '@home/components/calculated-fields/table-header/calculated-fields-filter-config.component';
import { CalculatedFieldComponent } from '@home/components/calculated-fields/calculated-field.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -57,7 +58,8 @@ import {
CalculatedFieldScriptTestDialogComponent, CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent, CalculatedFieldTestArgumentsComponent,
CalculatedFieldsHeaderComponent, CalculatedFieldsHeaderComponent,
CalculatedFieldsFilterConfigComponent CalculatedFieldsFilterConfigComponent,
CalculatedFieldComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -72,6 +74,7 @@ import {
exports: [ exports: [
CalculatedFieldDialogComponent, CalculatedFieldDialogComponent,
CalculatedFieldScriptTestDialogComponent, CalculatedFieldScriptTestDialogComponent,
CalculatedFieldComponent,
] ]
}) })
export class CalculatedFieldsModule {} export class CalculatedFieldsModule {}

112
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts

@ -20,7 +20,7 @@ import {
EntityTableColumn, EntityTableColumn,
EntityTableConfig EntityTableConfig
} from '@home/models/entity/entities-table-config.models'; } from '@home/models/entity/entities-table-config.models';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MatDialog, MatDialogRef } from '@angular/material/dialog';
@ -35,7 +35,7 @@ import { DestroyRef, Renderer2 } from '@angular/core';
import { EntityDebugSettings } from '@shared/models/entity.models'; import { EntityDebugSettings } from '@shared/models/entity.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { catchError, filter, switchMap, tap } from 'rxjs/operators'; import { catchError, filter, first, switchMap, tap } from 'rxjs/operators';
import { import {
ArgumentEntityType, ArgumentEntityType,
ArgumentType, ArgumentType,
@ -46,6 +46,7 @@ import {
CalculatedFieldsQuery, CalculatedFieldsQuery,
CalculatedFieldType, CalculatedFieldType,
CalculatedFieldTypeTranslations, CalculatedFieldTypeTranslations,
debugCfActionEnabled,
getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights, getCalculatedFieldArgumentsHighlights,
PropagationWithExpression, PropagationWithExpression,
@ -62,20 +63,26 @@ import { EntityDebugSettingsService } from '@home/components/entity/debug/entity
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { UtilsService } from "@core/services/utils.service"; import { UtilsService } from "@core/services/utils.service";
import { ActionNotificationShow } from "@core/notification/notification.actions"; import { ActionNotificationShow } from "@core/notification/notification.actions";
import { CalculatedFieldEventBody, DebugEventType, Event as DebugEvent, EventType } from '@shared/models/event.models'; import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models';
import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component'; import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component';
import { import {
CalculatedFieldsHeaderComponent CalculatedFieldsHeaderComponent
} from '@home/components/calculated-fields/table-header/calculated-fields-header.component'; } from '@home/components/calculated-fields/table-header/calculated-fields-header.component';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { CalculatedFieldComponent } from '@home/components/calculated-fields/calculated-field.component';
import { Router } from '@angular/router';
import { CalculatedFieldsTabsComponent } from '@home/pages/calculated-fields/calculated-fields-tabs.component';
type CalculatedFieldsTableEntity = CalculatedField | CalculatedFieldInfo; export type CalculatedFieldsTableEntity = CalculatedField | CalculatedFieldInfo;
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFieldsTableEntity> { export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFieldsTableEntity> {
readonly tenantId = getCurrentAuthUser(this.store).tenantId; readonly tenantId = getCurrentAuthUser(this.store).tenantId;
additionalDebugActionConfig = { additionalDebugActionConfig = {
title: this.translate.instant('action.see-debug-events'), title: this.translate.instant('action.see-debug-events'),
action: (calculatedField: CalculatedFieldsTableEntity) => this.openDebugEventsDialog.call(this, null, calculatedField), action: (calculatedField: CalculatedFieldsTableEntity,
openCalculatedFieldEdit = true,
afterCloseCallback?: (expression: string) => void) => this.openDebugEventsDialog.call(this, null, calculatedField, openCalculatedFieldEdit, afterCloseCallback),
}; };
calculatedFieldFilterConfig: CalculatedFieldsQuery; calculatedFieldFilterConfig: CalculatedFieldsQuery;
@ -93,30 +100,35 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
private importExportService: ImportExportService, private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService, private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService, private utilsService: UtilsService,
private router: Router,
public pageMode = false, public pageMode = false,
) { ) {
super(); super();
if (this.pageMode) { if (this.pageMode) {
this.headerComponent = CalculatedFieldsHeaderComponent; this.headerComponent = CalculatedFieldsHeaderComponent;
this.handleRowClick = ($event, entity) => { this.entityComponent = CalculatedFieldComponent;
this.editCalculatedField($event, entity); this.entityTabsComponent = CalculatedFieldsTabsComponent;
this.rowPointer = true; this.rowPointer = true;
return true;
};
} }
this.tableTitle = this.pageMode ? '' : this.translate.instant('entity.type-calculated-fields'); this.tableTitle = this.translate.instant('entity.type-calculated-fields');
this.detailsPanelEnabled = false; this.detailsPanelEnabled = this.pageMode;
this.entityType = EntityType.CALCULATED_FIELD; this.entityType = EntityType.CALCULATED_FIELD;
this.entityTranslations = entityTypeTranslations.get(EntityType.CALCULATED_FIELD); this.entityTranslations = entityTypeTranslations.get(EntityType.CALCULATED_FIELD);
this.entityResources = entityTypeResources.get(EntityType.CALCULATED_FIELD);
this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink); this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink);
this.addEntity = this.getCalculatedFieldDialog.bind(this); this.addEntity = this.getCalculatedFieldDialog.bind(this);
this.saveEntity = (cf) => this.calculatedFieldsService.saveCalculatedField(cf);
this.loadEntity = id => this.calculatedFieldsService.getCalculatedFieldById(id.id);
this.deleteEntityTitle = (field) => this.translate.instant('calculated-fields.delete-title', {title: field.name}); this.deleteEntityTitle = (field) => this.translate.instant('calculated-fields.delete-title', {title: field.name});
this.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text'); this.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text');
this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count}); this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count});
this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text'); this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text');
this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id); this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id);
this.onEntityAction = action => this.onCFAction(action);
this.addActionDescriptors = [ this.addActionDescriptors = [
{ {
name: this.translate.instant('calculated-fields.create'), name: this.translate.instant('calculated-fields.create'),
@ -163,7 +175,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
name: this.translate.instant('entity-view.events'), name: this.translate.instant('entity-view.events'),
icon: 'mdi:clipboard-text-clock', icon: 'mdi:clipboard-text-clock',
isEnabled: () => true, isEnabled: () => true,
onAction: ($event, entity) => this.openDebugEventsDialog($event, entity), onAction: ($event, entity) =>
this.pageMode ? this.openDebugTab($event, entity) : this.openDebugEventsDialog($event, entity),
}, },
{ {
name: '', name: '',
@ -172,14 +185,16 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
isEnabled: () => true, isEnabled: () => true,
iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline',
onAction: ($event, entity) => this.onOpenDebugConfig($event, entity), onAction: ($event, entity) => this.onOpenDebugConfig($event, entity),
}, }
{ );
if (!this.pageMode) {
this.cellActionDescriptors.push({
name: this.translate.instant('action.edit'), name: this.translate.instant('action.edit'),
icon: 'edit', icon: 'edit',
isEnabled: () => true, isEnabled: () => true,
onAction: ($event, entity) => this.editCalculatedField($event, entity), onAction: ($event, entity) => this.editCalculatedField($event, entity),
} })
); }
} }
fetchCalculatedFields(pageLink: PageLink): Observable<PageData<CalculatedFieldsTableEntity>> { fetchCalculatedFields(pageLink: PageLink): Observable<PageData<CalculatedFieldsTableEntity>> {
@ -188,7 +203,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
this.calculatedFieldsService.getCalculatedFieldsByEntityId(this.entityId, pageLink); this.calculatedFieldsService.getCalculatedFieldsByEntityId(this.entityId, pageLink);
} }
onOpenDebugConfig($event: Event, calculatedField: CalculatedFieldsTableEntity): void { private onOpenDebugConfig($event: Event, calculatedField: CalculatedFieldsTableEntity): void {
$event?.stopPropagation(); $event?.stopPropagation();
const { debugSettings = {}, id } = calculatedField; const { debugSettings = {}, id } = calculatedField;
const additionalActionConfig = { const additionalActionConfig = {
@ -212,6 +227,22 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
}, $event.target as Element); }, $event.target as Element);
} }
private openDebugTab($event: Event, calculatedField: CalculatedFieldsTableEntity) {
const table = this.getTable();
if (!table.isDetailsOpen) {
table.toggleEntityDetails($event, calculatedField);
if (table.entityDetailsPanel.matTabGroup._tabs.length > 1) {
table.entityDetailsPanel.matTabGroup.selectedIndex = 1;
} else {
table.entityDetailsPanel.matTabGroup._tabs.changes.pipe(
first()
).subscribe(() => {
table.entityDetailsPanel.matTabGroup.selectedIndex = 1;
})
}
}
}
private editCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity, isDirty = false): void { private editCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity, isDirty = false): void {
$event?.stopPropagation(); $event?.stopPropagation();
this.getCalculatedFieldDialog(calculatedField, 'action.apply', isDirty) this.getCalculatedFieldDialog(calculatedField, 'action.apply', isDirty)
@ -222,7 +253,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
}); });
} }
private getCalculatedFieldDialog(value?: CalculatedFieldsTableEntity, buttonTitle = 'action.add', isDirty = false): Observable<CalculatedField> { private getCalculatedFieldDialog(value?: CalculatedFieldsTableEntity, buttonTitle = 'action.add', isDirty = false, disabledSelectType = false): Observable<CalculatedField> {
const entityId = this.entityId || value?.entityId; const entityId = this.entityId || value?.entityId;
const entityName = this.entityName || (value as CalculatedFieldInfo)?.entityName; const entityName = this.entityName || (value as CalculatedFieldInfo)?.entityName;
return this.dialog.open<CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedField>(CalculatedFieldDialogComponent, { return this.dialog.open<CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedField>(CalculatedFieldDialogComponent, {
@ -234,10 +265,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
entityId, entityId,
entityName, entityName,
tenantId: this.tenantId, tenantId: this.tenantId,
ownerId: this.ownerId, ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId},
additionalDebugActionConfig: this.additionalDebugActionConfig, additionalDebugActionConfig: this.additionalDebugActionConfig,
getTestScriptDialogFn: this.getTestScriptDialog.bind(this), getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
isDirty, isDirty,
disabledSelectType
}, },
enterAnimationDuration: isDirty ? 0 : null, enterAnimationDuration: isDirty ? 0 : null,
}) })
@ -245,17 +277,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
.pipe(filter(Boolean)); .pipe(filter(Boolean));
} }
private openDebugEventsDialog($event: Event, calculatedField: CalculatedFieldsTableEntity): void { private openDebugEventsDialog($event: Event, calculatedField: CalculatedFieldsTableEntity, openCalculatedFieldEdit = true, afterCloseCallback?: (expression: string) => void ): void {
$event?.stopPropagation(); $event?.stopPropagation();
const debugActionEnabledFn = (event: DebugEvent) => {
return (calculatedField.type === CalculatedFieldType.SCRIPT ||
(calculatedField.type === CalculatedFieldType.PROPAGATION &&
calculatedField.configuration.applyExpressionToResolvedArguments)
) && !!(event as DebugEvent).body.arguments;
};
const onDebugEventSelected = (event: CalculatedFieldEventBody, dialogRef: MatDialogRef<EventsDialogComponent, string>) => { const onDebugEventSelected = (event: CalculatedFieldEventBody, dialogRef: MatDialogRef<EventsDialogComponent, string>) => {
this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments)) this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments), openCalculatedFieldEdit)
.subscribe(expression => dialogRef.close(expression)); .subscribe(expression => dialogRef.close(expression));
}; };
@ -270,11 +296,15 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS], disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS],
defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD, defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD,
onDebugEventSelected, onDebugEventSelected,
debugActionEnabledFn debugActionDisabled: !debugCfActionEnabled(calculatedField)
} }
}) })
.afterClosed() .afterClosed()
.subscribe(); .subscribe(value => {
if (afterCloseCallback) {
afterCloseCallback(value)
}
});
} }
private exportCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity): void { private exportCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity): void {
@ -315,7 +345,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
} }
return of(calculatedField); return of(calculatedField);
}), }),
switchMap(calculatedField => this.getCalculatedFieldDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)), switchMap(calculatedField => this.getCalculatedFieldDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true, true)),
filter(Boolean), filter(Boolean),
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)), switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)),
filter(Boolean), filter(Boolean),
@ -354,7 +384,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
).subscribe(() => this.updateData()); ).subscribe(() => this.updateData());
} }
private getTestScriptDialog(calculatedField: CalculatedFieldsTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable<string> { getTestScriptDialog(calculatedField: CalculatedFieldsTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable<string> {
if ( if (
calculatedField.type === CalculatedFieldType.SCRIPT || calculatedField.type === CalculatedFieldType.SCRIPT ||
calculatedField.type === CalculatedFieldType.RELATED_ENTITIES_AGGREGATION || calculatedField.type === CalculatedFieldType.RELATED_ENTITIES_AGGREGATION ||
@ -394,4 +424,22 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
return of(null); return of(null);
} }
} }
private openCalculatedField($event: Event, entity: CalculatedFieldsTableEntity) {
$event?.stopPropagation();
const url = this.router.createUrlTree(['calculatedFields', entity.id.id]);
this.router.navigateByUrl(url);
}
private onCFAction(action: EntityAction<CalculatedFieldsTableEntity>): boolean {
switch (action.action) {
case 'open':
this.openCalculatedField(action.event, action.entity);
return true;
case 'export':
this.exportCalculatedField(action.event, action.entity);
return true;
}
return false;
}
} }

4
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts

@ -36,7 +36,7 @@ import { ImportExportService } from '@shared/import-export/import-export.service
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { UtilsService } from "@core/services/utils.service"; import { UtilsService } from "@core/services/utils.service";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@Component({ @Component({
selector: 'tb-calculated-fields-table', selector: 'tb-calculated-fields-table',
@ -70,6 +70,7 @@ export class CalculatedFieldsTableComponent {
private utilsService: UtilsService, private utilsService: UtilsService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router
) { ) {
this.pageMode = !!this.route.snapshot.data.isPage; this.pageMode = !!this.route.snapshot.data.isPage;
effect(() => { effect(() => {
@ -88,6 +89,7 @@ export class CalculatedFieldsTableComponent {
this.importExportService, this.importExportService,
this.entityDebugSettingsService, this.entityDebugSettingsService,
this.utilsService, this.utilsService,
this.router,
this.pageMode, this.pageMode,
); );
this.cd.markForCheck(); this.cd.markForCheck();

6
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts

@ -259,7 +259,11 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
}; };
} }
if (!onInit) { if (!onInit) {
this.argumentFormGroup.get('refEntityKey').get('key').setValue(''); this.argumentFormGroup.get('refEntityKey').get('key').setValue('', {emitEvents: !this.watchKeyChange});
if (this.watchKeyChange && this.argumentFormGroup.get('argumentName').pristine) {
this.argumentFormGroup.get('argumentName').markAsUntouched({emitEvent: false});
this.argumentFormGroup.get('argumentName').setValue('', {emitEvent: false});
}
} else if (this.predefinedEntityFilter) { } else if (this.predefinedEntityFilter) {
entityFilter = this.predefinedEntityFilter; entityFilter = this.predefinedEntityFilter;
} }

8
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts

@ -154,7 +154,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
this.updateErrorText(); this.updateErrorText();
return this.errorText ? { argumentsFormArray: false } : null; return this.errorText || !this.argumentsFormArray.controls.length ? { argumentsFormArray: false } : null;
} }
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
@ -239,7 +239,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
} }
writeValue(argumentsObj: Record<string, CalculatedFieldArgument>): void { writeValue(argumentsObj: Record<string, CalculatedFieldArgument>): void {
this.argumentsFormArray.clear(); this.argumentsFormArray.clear({emitEvent: false});
this.populateArgumentsFormArray(argumentsObj); this.populateArgumentsFormArray(argumentsObj);
this.updateEntityNameMap(this.argumentsFormArray.value); this.updateEntityNameMap(this.argumentsFormArray.value);
} }
@ -249,7 +249,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
} }
protected changeIsScriptMode(): void { protected changeIsScriptMode(): void {
this.argumentsFormArray.updateValueAndValidity(); this.argumentsFormArray.updateValueAndValidity({emitEvent: !this.disable});
} }
protected isEditButtonShowBadge(argument: CalculatedFieldArgumentValue): boolean { protected isEditButtonShowBadge(argument: CalculatedFieldArgumentValue): boolean {
@ -264,7 +264,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
}; };
this.argumentsFormArray.push(this.fb.control(value), { emitEvent: false }); this.argumentsFormArray.push(this.fb.control(value), { emitEvent: false });
}); });
this.argumentsFormArray.updateValueAndValidity(); this.updateDataSource(this.argumentsFormArray.value);
} }
private updateEntityNameMap(values: CalculatedFieldArgumentValue[]): void { private updateEntityNameMap(values: CalculatedFieldArgumentValue[]): void {

32
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html

@ -56,29 +56,15 @@
/> />
</div> </div>
@if (!data.entityId) { @if (!data.entityId) {
<div class="flex flex-row gap-4 xs:flex-col" formGroupName="entityId"> <tb-entity-select formControlName="entityId"
<tb-entity-type-select #entityTypeSelect appearance="outline"
appearance="outline" required
class="flex-1" entityTypeLabel="{{ 'entity.entity-type' | translate }}"
showLabel [filterAllowedEntityTypes]="false"
required [allowedEntityTypes]="calculatedFieldsEntityTypeList"
label="{{ 'entity.entity-type' | translate }}" [defaultEntityType]="EntityType.DEVICE_PROFILE"
[filterAllowedEntityTypes]="false" (entityChanged)="changeEntity($event)">
[allowedEntityTypes]="calculatedFieldsEntityTypeList" </tb-entity-select>
formControlName="entityType">
</tb-entity-type-select>
@if (fieldFormGroup.get('entityId.entityType').value) {
<tb-entity-autocomplete #entityAutocompleteComponent
formControlName="id"
appearance="outline"
class="flex-1"
[placeholder]="'action.set' | translate"
[required]="true"
[entityType]="fieldFormGroup.get('entityId.entityType').value"
(entityChanged)="changeEntity($event)"
/>
}
</div>
} }
<mat-form-field appearance="outline" subscriptSizing="dynamic"> <mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'common.type' | translate }}</mat-label> <mat-label>{{ 'common.type' | translate }}</mat-label>

85
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts

@ -14,11 +14,10 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, DestroyRef, Inject, ViewChild, ViewEncapsulation } from '@angular/core'; import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { import {
@ -28,21 +27,18 @@ import {
CalculatedFieldTestScriptFn, CalculatedFieldTestScriptFn,
CalculatedFieldType, CalculatedFieldType,
calculatedFieldTypes, calculatedFieldTypes,
CalculatedFieldTypeTranslations, CalculatedFieldTypeTranslations
OutputStrategyType
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { EntityType } from '@shared/models/entity-type.models';
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
import { pairwise, switchMap } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
import { deepTrim, isDefined } from '@core/utils'; import { deepTrim } from '@core/utils';
import { EntityTypeSelectComponent } from '@shared/components/entity/entity-type-select.component';
import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component';
import { BaseData } from '@shared/models/base-data'; import { BaseData } from '@shared/models/base-data';
import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service';
import { FormGroup } from '@angular/forms';
export interface CalculatedFieldDialogData { export interface CalculatedFieldDialogData {
value?: CalculatedField; value?: CalculatedField;
@ -54,6 +50,7 @@ export interface CalculatedFieldDialogData {
additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>;
getTestScriptDialogFn: CalculatedFieldTestScriptFn; getTestScriptDialogFn: CalculatedFieldTestScriptFn;
isDirty?: boolean; isDirty?: boolean;
disabledSelectType?: boolean;
} }
@Component({ @Component({
@ -64,16 +61,7 @@ export interface CalculatedFieldDialogData {
}) })
export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFieldDialogComponent, CalculatedField> { export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFieldDialogComponent, CalculatedField> {
fieldFormGroup = this.fb.group({ fieldFormGroup: FormGroup;
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
entityId: this.fb.group({
entityType: this.fb.control<EntityType | AliasEntityType | null>(EntityType.DEVICE_PROFILE, Validators.required),
id: [null as null | string, Validators.required],
}),
type: [CalculatedFieldType.SIMPLE],
debugSettings: [],
configuration: this.fb.control<CalculatedFieldConfiguration>({} as CalculatedFieldConfiguration),
});
additionalDebugActionConfig = this.data.value?.id ? { additionalDebugActionConfig = this.data.value?.id ? {
...this.data.additionalDebugActionConfig, ...this.data.additionalDebugActionConfig,
@ -91,18 +79,16 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
readonly fieldTypes = calculatedFieldTypes; readonly fieldTypes = calculatedFieldTypes;
readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations; readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations;
@ViewChild('entityTypeSelect') entityTypeSelect: EntityTypeSelectComponent;
@ViewChild('entityAutocompleteComponent') entityAutocompleteComponent: EntityAutocompleteComponent;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDialogData, @Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDialogData,
protected dialogRef: MatDialogRef<CalculatedFieldDialogComponent, CalculatedField>, protected dialogRef: MatDialogRef<CalculatedFieldDialogComponent, CalculatedField>,
private calculatedFieldsService: CalculatedFieldsService, private calculatedFieldsService: CalculatedFieldsService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private fb: FormBuilder) { private cfFormService: CalculatedFieldFormService) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.observeType(); this.fieldFormGroup = this.cfFormService.buildForm();
this.cfFormService.setupTypeChange(this.fieldFormGroup, this.destroyRef);
this.applyDialogData(); this.applyDialogData();
if (this.data.isDirty) { if (this.data.isDirty) {
@ -110,7 +96,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
} }
if (!this.data.entityId) { if (!this.data.entityId) {
this.fieldFormGroup.get('entityId.id').valueChanges.pipe( this.fieldFormGroup.get('entityId').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe((entityId) => { ).subscribe((entityId) => {
this.disabledConfiguration = !entityId; this.disabledConfiguration = !entityId;
@ -121,10 +107,14 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
} }
}); });
} }
if (this.data.disabledSelectType) {
this.fieldFormGroup.get('type').disable({emitEvent: false});
}
} }
get fromGroupValue(): CalculatedField { get fromGroupValue(): CalculatedField {
return deepTrim(this.fieldFormGroup.value as CalculatedField); return deepTrim(this.fieldFormGroup.getRawValue() as CalculatedField);
} }
cancel(): void { cancel(): void {
@ -142,24 +132,17 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
}); });
} else { } else {
this.fieldFormGroup.get('name').markAsTouched(); this.fieldFormGroup.get('name').markAsTouched();
this.entityTypeSelect?.markAsTouched();
this.entityAutocompleteComponent?.markAsTouched();
} }
} }
onTestScript(expression?: string): Observable<string> { onTestScript(expression?: string): Observable<string> {
const calculatedFieldId = this.data.value?.id?.id; return this.cfFormService.testScript(
if (calculatedFieldId) { this.data.value?.id?.id,
return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true}) this.fromGroupValue,
.pipe( this.data.getTestScriptDialogFn,
switchMap(event => { this.destroyRef,
const args = event?.arguments ? JSON.parse(event.arguments) : null; expression
return this.data.getTestScriptDialogFn(this.fromGroupValue, args, false, expression); );
}),
takeUntilDestroyed(this.destroyRef)
)
}
return this.data.getTestScriptDialogFn(this.fromGroupValue, null, false, expression);
} }
changeEntity(entity: BaseData<EntityId>): void { changeEntity(entity: BaseData<EntityId>): void {
@ -172,28 +155,12 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
private applyDialogData(): void { private applyDialogData(): void {
const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.data.entityId, ...value } = this.data.value ?? {}; const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.data.entityId, ...value } = this.data.value ?? {};
if (configuration.type !== CalculatedFieldType.ALARM) { const preparedConfig = this.cfFormService.prepareConfig(configuration);
if (isDefined(configuration?.output) && !configuration?.output?.strategy) { this.fieldFormGroup.patchValue({ configuration: preparedConfig, type, debugSettings, entityId, ...value }, {emitEvent: false});
configuration.output.strategy = {type: OutputStrategyType.RULE_CHAIN};
}
}
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, entityId, ...value }, {emitEvent: false});
setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true})); setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true}));
if (!this.data.entityId) { if (!this.data.entityId) {
this.fieldFormGroup.get('configuration').disable({emitEvent: false}); this.fieldFormGroup.get('configuration').disable({emitEvent: false});
this.disabledConfiguration = true; this.disabledConfiguration = true;
} }
} }
private observeType(): void {
this.fieldFormGroup.get('type').valueChanges.pipe(
pairwise(),
takeUntilDestroyed(this.destroyRef)
).subscribe(([prevType, nextType]) => {
if (![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(prevType) ||
![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(nextType)) {
this.fieldFormGroup.get('configuration').setValue(({} as CalculatedFieldConfiguration), {emitEvent: false});
}
});
}
} }

10
ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts

@ -182,10 +182,12 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor
interval: {...value.interval, allowOffsetSec: isDefinedAndNotNull(value?.interval?.offsetSec)} interval: {...value.interval, allowOffsetSec: isDefinedAndNotNull(value?.interval?.offsetSec)}
} }
this.entityAggregationConfiguration.patchValue(data, {emitEvent: false}); this.entityAggregationConfiguration.patchValue(data, {emitEvent: false});
this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); if (this.entityAggregationConfiguration.enabled) {
this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value); this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value);
this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value);
this.checkProduceIntermediate(); this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value);
this.checkProduceIntermediate();
}
this.updatedOffsetHint(); this.updatedOffsetHint();
setTimeout(() => { setTimeout(() => {
this.entityAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); this.entityAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true});

6
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html

@ -95,7 +95,9 @@
<ng-container [formGroup]="refDynamicSourceFormGroup"> <ng-container [formGroup]="refDynamicSourceFormGroup">
<div class="tb-form-panel stroked" *ngIf="entityType === ArgumentEntityType.RelationQuery"> <div class="tb-form-panel stroked" *ngIf="entityType === ArgumentEntityType.RelationQuery">
<mat-expansion-panel class="tb-settings" expanded> <mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header>{{ 'calculated-fields.entity-zone-relationship' | translate }}</mat-expansion-panel-header> <mat-expansion-panel-header>
<span class="tb-form-panel-title tb-required">{{ 'calculated-fields.entity-zone-relationship' | translate }}</span>
</mat-expansion-panel-header>
<div class="tb-form-table"> <div class="tb-form-table">
<div class="tb-form-table-header"> <div class="tb-form-table-header">
<div class="tb-form-table-header-cell tb-actions-header"></div> <div class="tb-form-table-header-cell tb-actions-header"></div>
@ -221,7 +223,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
</ng-container> </ng-container>
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked" [class.!hidden]="entityType !== ArgumentEntityType.RelationQuery">
<mat-slide-toggle class="mat-slide" formControlName="createRelationsWithMatchedZones" (click)="$event.stopPropagation()"> <mat-slide-toggle class="mat-slide" formControlName="createRelationsWithMatchedZones" (click)="$event.stopPropagation()">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.create-relation-with-matched-zones' | translate }}"> <div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.create-relation-with-matched-zones' | translate }}">
{{ 'calculated-fields.create-relation-with-matched-zones' | translate }} {{ 'calculated-fields.create-relation-with-matched-zones' | translate }}

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts

@ -214,7 +214,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro
} }
writeValue(zonesObj: Record<string, CalculatedFieldGeofencing>): void { writeValue(zonesObj: Record<string, CalculatedFieldGeofencing>): void {
this.zoneGroupsFormArray.clear(); this.zoneGroupsFormArray.clear({emitEvent: false});
this.populateZonesFormArray(zonesObj); this.populateZonesFormArray(zonesObj);
this.updateEntityNameMap(this.zoneGroupsFormArray.value); this.updateEntityNameMap(this.zoneGroupsFormArray.value);
} }
@ -231,7 +231,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro
}; };
this.zoneGroupsFormArray.push(this.fb.control(value), { emitEvent: false }); this.zoneGroupsFormArray.push(this.fb.control(value), { emitEvent: false });
}); });
this.zoneGroupsFormArray.updateValueAndValidity(); this.updateDataSource(this.zoneGroupsFormArray.value);
} }
private updateEntityNameMap(values: CalculatedFieldGeofencingValue[]): void { private updateEntityNameMap(values: CalculatedFieldGeofencingValue[]): void {

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts

@ -124,7 +124,9 @@ export class GeofencingConfigurationComponent implements ControlValueAccessor, V
writeValue(config: CalculatedFieldGeofencingConfiguration): void { writeValue(config: CalculatedFieldGeofencingConfiguration): void {
this.geofencingConfiguration.patchValue(config, {emitEvent: false}); this.geofencingConfiguration.patchValue(config, {emitEvent: false});
this.checkRelatedEntity(this.geofencingConfiguration.get('zoneGroups').value); this.checkRelatedEntity(this.geofencingConfiguration.get('zoneGroups').value);
this.checkScheduledUpdateEnabled(this.geofencingConfiguration.get('scheduledUpdateEnabled').value); if (this.geofencingConfiguration.enabled) {
this.checkScheduledUpdateEnabled(this.geofencingConfiguration.get('scheduledUpdateEnabled').value);
}
} }
registerOnChange(fn: (config: CalculatedFieldGeofencingConfiguration) => void): void { registerOnChange(fn: (config: CalculatedFieldGeofencingConfiguration) => void): void {

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html

@ -170,7 +170,7 @@
[highlightRules]="highlightRules" [highlightRules]="highlightRules"
[editorCompleter]="editorCompleter" [editorCompleter]="editorCompleter"
[helpPopupStyle]="{ width: '1200px' }" [helpPopupStyle]="{ width: '1200px' }"
helpId="calculated-field/expression_fn"> helpId="calculated-field/map_expression_fn">
<div toolbarPrefixButton <div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }} class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}
</div> </div>

6
ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts

@ -74,7 +74,7 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit {
entityFilter: EntityFilter; entityFilter: EntityFilter;
readonly AggFunctions = Object.values(AggFunction) as AggFunction[]; AggFunctions = Object.values(AggFunction) as AggFunction[];
readonly AggFunctionTranslations = AggFunctionTranslations; readonly AggFunctionTranslations = AggFunctionTranslations;
readonly ScriptLanguage = ScriptLanguage; readonly ScriptLanguage = ScriptLanguage;
readonly AggInputType = AggInputType; readonly AggInputType = AggInputType;
@ -103,6 +103,10 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit {
this.validateInputKey(); this.validateInputKey();
this.functionArgs = ['ctx', ...this.arguments]; this.functionArgs = ['ctx', ...this.arguments];
if (this.simpleMode) {
this.AggFunctions = this.AggFunctions.filter(aggFunc => aggFunc !== AggFunction.COUNT_UNIQUE);
}
} }
saveMetric(): void { saveMetric(): void {

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts

@ -218,7 +218,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu
} }
writeValue(metrics: Record<string, CalculatedFieldAggMetric>): void { writeValue(metrics: Record<string, CalculatedFieldAggMetric>): void {
this.metricsFormArray.clear(); this.metricsFormArray.clear({emitEvent: false});
this.populateZonesFormArray(metrics); this.populateZonesFormArray(metrics);
} }
@ -230,7 +230,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu
}; };
this.metricsFormArray.push(this.fb.control(value), { emitEvent: false }); this.metricsFormArray.push(this.fb.control(value), { emitEvent: false });
}); });
this.metricsFormArray.updateValueAndValidity(); this.updateDataSource(this.metricsFormArray.value);
} }
private getSortValue(metric: CalculatedFieldAggMetricValue, column: string): string { private getSortValue(metric: CalculatedFieldAggMetricValue, column: string): string {

6
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts

@ -148,7 +148,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
for (const propName of Object.keys(changes)) { for (const propName of Object.keys(changes)) {
const change = changes[propName]; const change = changes[propName];
if (change.currentValue !== change.previousValue) { if (change.currentValue !== change.previousValue) {
if (propName === 'simpleMode') { if (propName === 'simpleMode' && !this.disabled) {
this.updatedFormWithMode(); this.updatedFormWithMode();
if (!change.firstChange) { if (!change.firstChange) {
this.outputForm.updateValueAndValidity(); this.outputForm.updateValueAndValidity();
@ -167,7 +167,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
if (value.type === OutputType.Timeseries && value.strategy?.type === OutputStrategyType.IMMEDIATE && value.strategy?.ttl) { if (value.type === OutputType.Timeseries && value.strategy?.type === OutputStrategyType.IMMEDIATE && value.strategy?.ttl) {
this.outputForm.get('strategy.useCustomTtl').setValue(true, {emitEvent: false}); this.outputForm.get('strategy.useCustomTtl').setValue(true, {emitEvent: false});
} }
this.outputForm.get('type').updateValueAndValidity({onlySelf: true}); this.outputForm.get('type').updateValueAndValidity({onlySelf: true, emitEvent: false});
} }
registerOnChange(fn: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void): void { registerOnChange(fn: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void): void {
@ -253,7 +253,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
private updateTimeSeriesTtl(value: boolean) { private updateTimeSeriesTtl(value: boolean) {
if (value) { if (value) {
this.outputForm.get('strategy.useCustomTtl').enable({emitEvent: true}); this.outputForm.get('strategy.useCustomTtl').enable({emitEvent: false});
} else { } else {
this.outputForm.get('strategy.useCustomTtl').disable({emitEvent: false}); this.outputForm.get('strategy.useCustomTtl').disable({emitEvent: false});
this.outputForm.get('strategy.ttl').disable({emitEvent: false}); this.outputForm.get('strategy.ttl').disable({emitEvent: false});

6
ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts

@ -131,9 +131,11 @@ export class PropagationConfigurationComponent implements ControlValueAccessor,
writeValue(value: PropagationWithExpression): void { writeValue(value: PropagationWithExpression): void {
value.expression = value.expression ?? calculatedFieldDefaultScript; value.expression = value.expression ?? calculatedFieldDefaultScript;
this.propagateConfiguration.patchValue(value, {emitEvent: false}); this.propagateConfiguration.patchValue(value, {emitEvent: false});
this.updatedFormWithScript(); if (!this.disabled) {
this.updatedFormWithScript();
}
setTimeout(() => { setTimeout(() => {
this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true, emitEvent: false});
}); });
} }

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts

@ -151,7 +151,6 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
formValue.expressionSIMPLE = formValue.expression; formValue.expressionSIMPLE = formValue.expression;
} }
this.simpleConfiguration.patchValue(formValue, {emitEvent: false}); this.simpleConfiguration.patchValue(formValue, {emitEvent: false});
this.updatedFormWithScript();
setTimeout(() => { setTimeout(() => {
this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true});
}); });

1
ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts

@ -204,6 +204,7 @@ export class CalculatedFieldsFilterConfigComponent implements OnInit, ControlVal
cancel() { cancel() {
this.updateCfConfigForm(this.cfFilterConfig); this.updateCfConfigForm(this.cfFilterConfig);
this.cfFilterForm.markAsPristine();
if (this.overlayRef) { if (this.overlayRef) {
this.overlayRef.dispose(); this.overlayRef.dispose();
} else { } else {

1
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -548,6 +548,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
const cssParser = new cssjs(); const cssParser = new cssjs();
cssParser.testMode = false; cssParser.testMode = false;
this.dashboardPageClass = 'tb-dashboard-page-css-' + guid(); this.dashboardPageClass = 'tb-dashboard-page-css-' + guid();
this.dashboardCtx.dashboardCssClass = this.dashboardPageClass;
cssParser.cssPreviewNamespace = 'tb-default .' + this.dashboardPageClass; cssParser.cssPreviewNamespace = 'tb-default .' + this.dashboardPageClass;
cssParser.createStyleElement(this.dashboardPageClass, cssString); cssParser.createStyleElement(this.dashboardPageClass, cssString);
} }

1
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts

@ -51,6 +51,7 @@ export interface DashboardContext {
stateChanged: Observable<string>; stateChanged: Observable<string>;
stateId: Observable<string>; stateId: Observable<string>;
runChangeDetection: () => void; runChangeDetection: () => void;
dashboardCssClass?: string;
} }
export interface IDashboardController { export interface IDashboardController {

1
ui-ngx/src/app/modules/home/components/device/device-info-filter.component.ts

@ -196,6 +196,7 @@ export class DeviceInfoFilterComponent implements OnInit, ControlValueAccessor {
cancel() { cancel() {
this.updateDeviceInfoFilterForm(this.deviceInfoFilter); this.updateDeviceInfoFilterForm(this.deviceInfoFilter);
this.deviceInfoFilterForm.markAsPristine();
if (this.overlayRef) { if (this.overlayRef) {
this.overlayRef.dispose(); this.overlayRef.dispose();
} else { } else {

4
ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts

@ -22,7 +22,9 @@ import { TranslateService } from '@ngx-translate/core';
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { EntityDebugSettingPanelConfig } from '@home/components/entity/debug/entity-debug-settings.model'; import { EntityDebugSettingPanelConfig } from '@home/components/entity/debug/entity-debug-settings.model';
@Injectable() @Injectable({
providedIn: 'root'
})
export class EntityDebugSettingsService { export class EntityDebugSettingsService {
constructor( constructor(

10
ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts

@ -16,7 +16,7 @@
import { BaseData, HasId } from '@shared/models/base-data'; import { BaseData, HasId } from '@shared/models/base-data';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { AfterViewInit, Directive, Input, OnInit, QueryList, ViewChildren } from '@angular/core'; import { AfterViewInit, Directive, inject, Input, OnInit, QueryList, ViewChildren } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
@ -100,9 +100,11 @@ export abstract class EntityTabsComponent<T extends BaseData<HasId>,
entityTabsChanged = this.entityTabsSubject.asObservable(); entityTabsChanged = this.entityTabsSubject.asObservable();
protected constructor(protected store: Store<AppState>) { protected store: Store<AppState> = inject(Store<AppState>);
super(store);
this.authUser = getCurrentAuthUser(store); protected constructor(...args: unknown[]) {
super();
this.authUser = getCurrentAuthUser(this.store);
} }
ngOnInit() { ngOnInit() {

16
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -22,7 +22,14 @@ import {
EntityTableColumn, EntityTableColumn,
EntityTableConfig EntityTableConfig
} from '@home/models/entity/entities-table-config.models'; } from '@home/models/entity/entities-table-config.models';
import { DebugEventType, Event, EventBody, EventType, FilterEventBody } from '@shared/models/event.models'; import {
DebugEventType,
Event as DebugEvent,
Event,
EventBody,
EventType,
FilterEventBody
} from '@shared/models/event.models';
import { TimePageLink } from '@shared/models/page/page-link'; import { TimePageLink } from '@shared/models/page/page-link';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
@ -95,7 +102,8 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
private store: Store<AppState>, private store: Store<AppState>,
public testButtonLabel?: string, public testButtonLabel?: string,
private debugEventSelected?: EventEmitter<EventBody>, private debugEventSelected?: EventEmitter<EventBody>,
public hideClearEventAction = false) { public hideClearEventAction = false,
public disableDebugEventAction = false) {
super(); super();
this.loadDataOnInit = false; this.loadDataOnInit = false;
this.tableTitle = ''; this.tableTitle = '';
@ -453,7 +461,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
this.cellActionDescriptors.push({ this.cellActionDescriptors.push({
name: this.translate.instant('rulenode.test-with-this-message', {test: this.translate.instant(this.testButtonLabel)}), name: this.translate.instant('rulenode.test-with-this-message', {test: this.translate.instant(this.testButtonLabel)}),
icon: 'bug_report', icon: 'bug_report',
isEnabled: (entity) => entity.body.type === 'IN' || entity.body.error !== undefined, isEnabled: (entity) => (entity.body.type === 'IN' || entity.body.error !== undefined) && !this.disableDebugEventAction,
onAction: ($event, entity) => { onAction: ($event, entity) => {
this.debugEventSelected.next(entity.body); this.debugEventSelected.next(entity.body);
} }
@ -464,7 +472,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
this.cellActionDescriptors.push({ this.cellActionDescriptors.push({
name: this.translate.instant('calculated-fields.test-with-this-message'), name: this.translate.instant('calculated-fields.test-with-this-message'),
icon: 'bug_report', icon: 'bug_report',
isEnabled: () => true, isEnabled: (event) => !this.disableDebugEventAction && !!(event as DebugEvent).body.arguments,
onAction: (_, entity) => this.debugEventSelected.next(entity.body) onAction: (_, entity) => this.debugEventSelected.next(entity.body)
}); });
break; break;

24
ui-ngx/src/app/modules/home/components/event/event-table.component.ts

@ -17,10 +17,12 @@
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectorRef, ChangeDetectorRef,
Component, EventEmitter, Component,
EventEmitter,
Input, Input,
OnDestroy, OnDestroy,
OnInit, Output, OnInit,
Output,
ViewChild, ViewChild,
ViewContainerRef ViewContainerRef
} from '@angular/core'; } from '@angular/core';
@ -61,6 +63,21 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
@Input() @Input()
hideClearEventAction: boolean = false; hideClearEventAction: boolean = false;
private disableDebugEventActionValue = false;
get disableDebugEventAction() {
return this.disableDebugEventActionValue;
}
@Input()
set disableDebugEventAction(value) {
this.disableDebugEventActionValue = value;
if (this.eventTableConfig) {
this.eventTableConfig.disableDebugEventAction = this.disableDebugEventAction;
this.eventTableConfig.updateCellAction();
}
};
activeValue = false; activeValue = false;
dirtyValue = false; dirtyValue = false;
entityIdValue: EntityId; entityIdValue: EntityId;
@ -151,7 +168,8 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
this.store, this.store,
this.functionTestButtonLabel, this.functionTestButtonLabel,
this.debugEventSelected, this.debugEventSelected,
this.hideClearEventAction this.hideClearEventAction,
this.disableDebugEventAction
); );
} }

6
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-bootstrap-config-servers.component.ts

@ -30,7 +30,7 @@ import { TranslateService } from '@ngx-translate/core';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { Lwm2mBootstrapAddConfigServerDialogComponent } from '@home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component'; import { Lwm2mBootstrapAddConfigServerDialogComponent } from '@home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component';
import { mergeMap, takeUntil } from 'rxjs/operators'; import { filter, mergeMap, takeUntil } from 'rxjs/operators';
import { DeviceProfileService } from '@core/http/device-profile.service'; import { DeviceProfileService } from '@core/http/device-profile.service';
import { Lwm2mSecurityType } from '@shared/models/lwm2m-security-config.models'; import { Lwm2mSecurityType } from '@shared/models/lwm2m-security-config.models';
@ -164,10 +164,8 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed(); }).afterClosed();
const addServerConfigObs = addDialogObs.pipe( const addServerConfigObs = addDialogObs.pipe(
filter((isBootstrap) => isBootstrap !== null),
mergeMap((isBootstrap) => { mergeMap((isBootstrap) => {
if (isBootstrap === null) {
return of(null);
}
return this.deviceProfileService.getLwm2mBootstrapSecurityInfoBySecurityType(isBootstrap, Lwm2mSecurityType.NO_SEC); return this.deviceProfileService.getLwm2mBootstrapSecurityInfoBySecurityType(isBootstrap, Lwm2mSecurityType.NO_SEC);
}) })
); );

2
ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts

@ -968,7 +968,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
(AlarmDetailsDialogComponent, (AlarmDetailsDialogComponent,
{ {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog', this.ctx.stateController.dashboardCtrl.dashboardCtx.dashboardCssClass, this.ctx.widgetCssClass],
data: { data: {
alarmId: alarm.id.id, alarmId: alarm.id.id,
allowAcknowledgment: this.allowAcknowledgment, allowAcknowledgment: this.allowAcknowledgment,

7
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts

@ -113,9 +113,6 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
this.colorRangeListFormGroup.valueChanges.pipe( this.colorRangeListFormGroup.valueChanges.pipe(
takeUntil(this.destroy$) takeUntil(this.destroy$)
).subscribe(() => this.updateModel()); ).subscribe(() => this.updateModel());
this.colorRangeListFormGroup.get('advancedMode').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => setTimeout(() => {this.popover?.updatePosition();}, 0));
} }
ngOnDestroy() { ngOnDestroy() {
@ -179,7 +176,6 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
public removeAdvancedRange(index: number) { public removeAdvancedRange(index: number) {
(this.colorRangeListFormGroup.get('rangeAdvanced') as UntypedFormArray).removeAt(index); (this.colorRangeListFormGroup.get('rangeAdvanced') as UntypedFormArray).removeAt(index);
setTimeout(() => {this.popover?.updatePosition();}, 0);
} }
get advancedRangeFormArray(): UntypedFormArray { get advancedRangeFormArray(): UntypedFormArray {
@ -193,7 +189,6 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
removeRange(index: number) { removeRange(index: number) {
this.rangeListFormArray.removeAt(index); this.rangeListFormArray.removeAt(index);
this.colorRangeListFormGroup.markAsDirty(); this.colorRangeListFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
} }
rangeDrop(event: CdkDragDrop<string[]>, range: string) { rangeDrop(event: CdkDragDrop<string[]>, range: string) {
@ -216,7 +211,6 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
const advancedRangeColorsArray = this.colorRangeListFormGroup.get('rangeAdvanced') as UntypedFormArray; const advancedRangeColorsArray = this.colorRangeListFormGroup.get('rangeAdvanced') as UntypedFormArray;
const advancedRangeColorControl = this.fb.control(advancedRange, [advancedRangeValidator]); const advancedRangeColorControl = this.fb.control(advancedRange, [advancedRangeValidator]);
advancedRangeColorsArray.push(advancedRangeColorControl); advancedRangeColorsArray.push(advancedRangeColorControl);
setTimeout(() => {this.popover?.updatePosition();}, 0);
} }
addRange() { addRange() {
@ -228,7 +222,6 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
}; };
this.rangeListFormArray.push(this.colorRangeControl(newRange)); this.rangeListFormArray.push(this.colorRangeControl(newRange));
this.colorRangeListFormGroup.markAsDirty(); this.colorRangeListFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
} }
} }

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html

@ -94,7 +94,7 @@
</div> </div>
<ng-template #function> <ng-template #function>
<div class="tb-form-panel no-padding no-border" [formGroup]="colorSettingsFormGroup"> <div class="tb-form-panel no-padding no-border overflow-auto" [formGroup]="colorSettingsFormGroup">
<tb-js-func formControlName="colorFunction" <tb-js-func formControlName="colorFunction"
[functionArgs]="['value']" [functionArgs]="['value']"
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"

7
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts

@ -34,7 +34,6 @@ import { IAliasController } from '@core/api/widget-api.models';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { Datasource } from '@shared/models/widget.models'; import { Datasource } from '@shared/models/widget.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ @Component({
selector: 'tb-color-settings-panel', selector: 'tb-color-settings-panel',
@ -107,12 +106,6 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit
colorFunction: [this.colorSettings?.colorFunction, []] colorFunction: [this.colorSettings?.colorFunction, []]
} }
); );
this.colorSettingsFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateValidators();
setTimeout(() => {this.popover?.updatePosition();}, 0);
});
this.updateValidators(); this.updateValidators();
} }

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts

@ -117,9 +117,6 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
this.gradientFormGroup.valueChanges.pipe( this.gradientFormGroup.valueChanges.pipe(
takeUntil(this.destroy$) takeUntil(this.destroy$)
).subscribe(() => this.updateModel()); ).subscribe(() => this.updateModel());
this.gradientFormGroup.get('advancedMode').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => setTimeout(() => {this.popover?.updatePosition();}, 0));
} }
ngOnDestroy() { ngOnDestroy() {
@ -236,7 +233,6 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
this.gradientListFormArray.removeAt(index); this.gradientListFormArray.removeAt(index);
} }
this.gradientFormGroup.markAsDirty(); this.gradientFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
} }
gradientDrop(event: CdkDragDrop<string[]>, advanced = false) { gradientDrop(event: CdkDragDrop<string[]>, advanced = false) {
@ -255,7 +251,6 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
this.gradientListFormArray.push(this.colorGradientControl('rgba(0,0,0,0.87)')); this.gradientListFormArray.push(this.colorGradientControl('rgba(0,0,0,0.87)'));
} }
this.gradientFormGroup.markAsDirty(); this.gradientFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
} }
updateModel() { updateModel() {

4
ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts

@ -151,8 +151,8 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
this.widget.widgetContext.containerChangeDetector = this.cd; this.widget.widgetContext.containerChangeDetector = this.cd;
const cssString = this.widget.widget.config.widgetCss; const cssString = this.widget.widget.config.widgetCss;
if (isNotEmptyStr(cssString)) { if (isNotEmptyStr(cssString)) {
this.cssClass = this.cssClass = this.utils.applyCssToElement(this.renderer, this.gridsterItem.el, 'tb-widget-css', cssString, true);
this.utils.applyCssToElement(this.renderer, this.gridsterItem.el, 'tb-widget-css', cssString); this.widget.widgetContext.widgetCssClass = this.cssClass;
} }
$(this.gridsterItem.el).on('mousedown', (e) => this.onMouseDown(e.originalEvent)); $(this.gridsterItem.el).on('mousedown', (e) => this.onMouseDown(e.originalEvent));
$(this.gridsterItem.el).on('click', (e) => this.onClicked(e.originalEvent)); $(this.gridsterItem.el).on('click', (e) => this.onClicked(e.originalEvent));

1
ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html

@ -34,6 +34,7 @@
[tenantId]="data.tenantId" [tenantId]="data.tenantId"
[entityId]="data.entityId" [entityId]="data.entityId"
[functionTestButtonLabel]="data.functionTestButtonLabel ?? ''" [functionTestButtonLabel]="data.functionTestButtonLabel ?? ''"
[disableDebugEventAction]="data.debugActionDisabled"
(debugEventSelected)="this.onDebugEventSelected($event)" (debugEventSelected)="this.onDebugEventSelected($event)"
/> />
</div> </div>

6
ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts

@ -23,7 +23,6 @@ import { AppState } from '@core/core.state';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DebugEventType, EventType } from '@shared/models/event.models'; import { DebugEventType, EventType } from '@shared/models/event.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { BaseData, HasId } from '@shared/models/base-data';
export interface EventsDialogData { export interface EventsDialogData {
title: string; title: string;
@ -34,7 +33,7 @@ export interface EventsDialogData {
disabledEventTypes?: Array<EventType | DebugEventType>; disabledEventTypes?: Array<EventType | DebugEventType>;
functionTestButtonLabel?: string; functionTestButtonLabel?: string;
onDebugEventSelected?: (event: any, dialogRef: MatDialogRef<EventsDialogComponent, string>) => void; onDebugEventSelected?: (event: any, dialogRef: MatDialogRef<EventsDialogComponent, string>) => void;
debugActionEnabledFn?: (event: BaseData<HasId>) => boolean; debugActionDisabled?: boolean;
} }
@Component({ @Component({
@ -54,9 +53,6 @@ export class EventsDialogComponent extends DialogComponent<EventsDialogComponent
} }
ngAfterViewInit() { ngAfterViewInit() {
if (this.data.debugActionEnabledFn && this.eventsTable.entitiesTable.cellActionDescriptors?.length > 0) {
this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = this.data.debugActionEnabledFn;
}
this.eventsTable.entitiesTable.updateData(); this.eventsTable.entitiesTable.updateData();
} }

9
ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts

@ -20,18 +20,22 @@ import { SafeHtml } from '@angular/platform-browser';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { Timewindow } from '@shared/models/time/time.models'; import { Timewindow } from '@shared/models/time/time.models';
import { EntitiesDataSource } from '@home/models/datasource/entity-datasource'; import { EntitiesDataSource } from '@home/models/datasource/entity-datasource';
import { ElementRef, EventEmitter, Renderer2, ViewContainerRef } from '@angular/core'; import { ElementRef, EventEmitter, ViewContainerRef } from '@angular/core';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { EntityAction } from '@home/models/entity/entity-component.models'; import { EntityAction } from '@home/models/entity/entity-component.models';
import { import {
CellActionDescriptor, EntityActionTableColumn, EntityColumn, EntityTableColumn, CellActionDescriptor,
EntityActionTableColumn,
EntityColumn,
EntityTableColumn,
EntityTableConfig, EntityTableConfig,
GroupActionDescriptor, GroupActionDescriptor,
HeaderActionDescriptor HeaderActionDescriptor
} from '@home/models/entity/entities-table-config.models'; } from '@home/models/entity/entities-table-config.models';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import type { EntityDetailsPanelComponent } from '@home/components/entity/entity-details-panel.component';
export type EntitiesTableAction = 'add'; export type EntitiesTableAction = 'add';
@ -64,6 +68,7 @@ export interface IEntitiesTableComponent {
paginator: MatPaginator; paginator: MatPaginator;
sort: MatSort; sort: MatSort;
route: ActivatedRoute; route: ActivatedRoute;
entityDetailsPanel: EntityDetailsPanelComponent;
viewContainerRef: ViewContainerRef; viewContainerRef: ViewContainerRef;
addEnabled(): boolean; addEnabled(): boolean;

2
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -308,6 +308,8 @@ export class WidgetContext {
widgetNamespace?: string; widgetNamespace?: string;
subscriptionApi?: WidgetSubscriptionApi; subscriptionApi?: WidgetSubscriptionApi;
widgetCssClass?: string;
actionsApi?: WidgetActionsApi; actionsApi?: WidgetActionsApi;
activeEntityInfo?: SubscriptionEntityInfo; activeEntityInfo?: SubscriptionEntityInfo;

87
ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts

@ -14,14 +14,64 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { NgModule } from '@angular/core'; import { DestroyRef, inject, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { ActivatedRouteSnapshot, ResolveFn, Router, RouterModule, RouterStateSnapshot, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { AlarmTableComponent } from '@home/components/alarm/alarm-table.component'; import { AlarmTableComponent } from '@home/components/alarm/alarm-table.component';
import { AlarmsMode } from '@shared/models/alarm.models'; import { AlarmsMode } from '@shared/models/alarm.models';
import { MenuId } from '@core/services/menu.models'; import { MenuId } from '@core/services/menu.models';
import { RouterTabsComponent } from "@home/components/router-tabs.component"; import { RouterTabsComponent } from "@home/components/router-tabs.component";
import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rules-table.component"; import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rules-table.component";
import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { CalculatedFieldsTableConfigResolver } from '@home/pages/calculated-fields/calculated-fields-routing.module';
import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/calculated-fields-table-config';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DatePipe } from '@angular/common';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { UtilsService } from '@core/services/utils.service';
import { AlarmRulesTableConfig } from '@home/components/alarm-rules/alarm-rules-table-config';
export const AlarmRulesTableConfigResolver: ResolveFn<AlarmRulesTableConfig> =
(_route: ActivatedRouteSnapshot,
_state: RouterStateSnapshot,
calculatedFieldsService = inject(CalculatedFieldsService),
translate = inject(TranslateService),
dialog = inject(MatDialog),
store = inject(Store<AppState>),
datePipe = inject(DatePipe),
destroyRef = inject(DestroyRef),
importExportService = inject(ImportExportService),
entityDebugSettingsService = inject(EntityDebugSettingsService),
utilsService = inject(UtilsService),
router = inject(Router),
) => {
return new AlarmRulesTableConfig(
calculatedFieldsService,
translate,
dialog,
datePipe,
null,
store,
destroyRef,
null,
null,
null,
importExportService,
entityDebugSettingsService,
utilsService,
router,
true,
);
};
const routes: Routes = [ const routes: Routes = [
{ {
@ -57,15 +107,38 @@ const routes: Routes = [
}, },
{ {
path: 'alarm-rules', path: 'alarm-rules',
component: AlarmRulesTableComponent,
data: { data: {
auth: [Authority.TENANT_ADMIN],
title: 'alarm-rule.alarm-rules',
breadcrumb: { breadcrumb: {
menuId: MenuId.alarm_rules menuId: MenuId.alarm_rules
}, },
isPage: true, },
} children: [
{
path: '',
component: AlarmRulesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'alarm-rule.alarm-rules',
isPage: true,
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'tune'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN],
title: 'entity.type-calculated-fields',
},
resolve: {
entitiesTableConfig: AlarmRulesTableConfigResolver
}
}
]
} }
] ]
} }

30
ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.html

@ -0,0 +1,30 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
@if (entity) {
<mat-tab label="{{ 'calculated-fields.events' | translate }}" #eventsTab="matTab">
<tb-event-table [defaultEventType]="DebugEventType.DEBUG_CALCULATED_FIELD"
[disabledEventTypes]="[EventType.LC_EVENT, EventType.ERROR, EventType.STATS]"
[debugEventTypes]="[DebugEventType.DEBUG_CALCULATED_FIELD]"
[active]="eventsTab.isActive"
[tenantId]="entity.tenantId.id"
[entityId]="entity.id"
[disableDebugEventAction]="debugActionDisabled"
(debugEventSelected)="onDebugEventSelected($event)"
></tb-event-table>
</mat-tab>
}

49
ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts

@ -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.
///
import { Component } from '@angular/core';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models';
import type {
CalculatedFieldsTableConfig,
CalculatedFieldsTableEntity
} from '@home/components/calculated-fields/calculated-fields-table-config';
import { debugCfActionEnabled } from '@shared/models/calculated-field.models';
@Component({
selector: 'tb-alarm-rules-tabs',
templateUrl: './alarm-rules-tabs.component.html',
styleUrls: []
})
export class AlarmRulesTabsComponent extends EntityTabsComponent<CalculatedFieldsTableEntity> {
readonly DebugEventType = DebugEventType;
readonly EventType = EventType;
constructor() {
super();
}
get debugActionDisabled(): boolean {
return !debugCfActionEnabled(this.entity);
};
onDebugEventSelected(event: CalculatedFieldEventBody) {
(this.entitiesTableConfig as CalculatedFieldsTableConfig).getTestScriptDialog(this.entity, JSON.parse(event.arguments))
.subscribe((expression) => {
});
};
}

5
ui-ngx/src/app/modules/home/pages/alarm/alarm.module.ts

@ -20,9 +20,12 @@ import { SharedModule } from '@shared/shared.module';
import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; import { HomeDialogsModule } from '../../dialogs/home-dialogs.module';
import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { AlarmRoutingModule } from '@home/pages/alarm/alarm-routing.module'; import { AlarmRoutingModule } from '@home/pages/alarm/alarm-routing.module';
import { AlarmRulesTabsComponent } from '@home/pages/alarm/alarm-rules-tabs.component';
@NgModule({ @NgModule({
declarations: [], declarations: [
AlarmRulesTabsComponent
],
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,

10
ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields.module.ts → ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-field-page.module.ts

@ -20,15 +20,19 @@ import { SharedModule } from '@shared/shared.module';
import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; import { HomeDialogsModule } from '../../dialogs/home-dialogs.module';
import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { CalculatedFieldsRoutingModule } from '@home/pages/calculated-fields/calculated-fields-routing.module'; import { CalculatedFieldsRoutingModule } from '@home/pages/calculated-fields/calculated-fields-routing.module';
import { CalculatedFieldsModule } from '@home/components/calculated-fields/calculated-field.module';
import { CalculatedFieldsTabsComponent } from '@home/pages/calculated-fields/calculated-fields-tabs.component';
@NgModule({ @NgModule({
declarations: [], declarations: [
CalculatedFieldsTabsComponent
],
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
HomeComponentsModule, HomeComponentsModule,
HomeDialogsModule, HomeDialogsModule,
CalculatedFieldsRoutingModule CalculatedFieldsRoutingModule,
] ]
}) })
export class CalculatedFieldsModule { } export class CalculatedFieldPageModule { }

84
ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-routing.module.ts

@ -14,24 +14,94 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { NgModule } from '@angular/core'; import { DestroyRef, inject, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { ActivatedRouteSnapshot, ResolveFn, Router, RouterModule, RouterStateSnapshot, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { MenuId } from '@core/services/menu.models'; import { MenuId } from '@core/services/menu.models';
import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component'; import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/calculated-fields-table-config';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DatePipe } from '@angular/common';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { UtilsService } from '@core/services/utils.service';
export const CalculatedFieldsTableConfigResolver: ResolveFn<CalculatedFieldsTableConfig> =
(_route: ActivatedRouteSnapshot,
_state: RouterStateSnapshot,
calculatedFieldsService = inject(CalculatedFieldsService),
translate = inject(TranslateService),
dialog = inject(MatDialog),
store = inject(Store<AppState>),
datePipe = inject(DatePipe),
destroyRef = inject(DestroyRef),
importExportService = inject(ImportExportService),
entityDebugSettingsService = inject(EntityDebugSettingsService),
utilsService = inject(UtilsService),
router = inject(Router),
) => {
return new CalculatedFieldsTableConfig(
calculatedFieldsService,
translate,
dialog,
datePipe,
null,
store,
destroyRef,
null,
null,
null,
importExportService,
entityDebugSettingsService,
utilsService,
router,
true,
);
};
const routes: Routes = [ const routes: Routes = [
{ {
path: 'calculatedFields', path: 'calculatedFields',
component: CalculatedFieldsTableComponent,
data: { data: {
auth: [Authority.TENANT_ADMIN],
title: 'entity.type-calculated-fields',
breadcrumb: { breadcrumb: {
menuId: MenuId.calculated_fields menuId: MenuId.calculated_fields
}
},
children: [
{
path: '',
component: CalculatedFieldsTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'entity.type-calculated-fields',
isPage: true,
}
}, },
isPage: true, {
} path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'mdi:function-variant'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN],
title: 'entity.type-calculated-fields',
},
resolve: {
entitiesTableConfig: CalculatedFieldsTableConfigResolver
}
}
]
} }
]; ];

30
ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.html

@ -0,0 +1,30 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
@if (entity) {
<mat-tab label="{{ 'calculated-fields.events' | translate }}" #eventsTab="matTab">
<tb-event-table [defaultEventType]="DebugEventType.DEBUG_CALCULATED_FIELD"
[disabledEventTypes]="[EventType.LC_EVENT, EventType.ERROR, EventType.STATS]"
[debugEventTypes]="[DebugEventType.DEBUG_CALCULATED_FIELD]"
[active]="eventsTab.isActive"
[tenantId]="entity.tenantId.id"
[entityId]="entity.id"
[disableDebugEventAction]="debugActionDisabled"
(debugEventSelected)="onDebugEventSelected($event)"
></tb-event-table>
</mat-tab>
}

57
ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.ts

@ -0,0 +1,57 @@
///
/// 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 } from '@angular/core';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models';
import type {
CalculatedFieldsTableConfig,
CalculatedFieldsTableEntity
} from '@home/components/calculated-fields/calculated-fields-table-config';
import { debugCfActionEnabled } from '@shared/models/calculated-field.models';
@Component({
selector: 'tb-calculated-fields-tabs',
templateUrl: './calculated-fields-tabs.component.html',
styleUrls: []
})
export class CalculatedFieldsTabsComponent extends EntityTabsComponent<CalculatedFieldsTableEntity> {
readonly DebugEventType = DebugEventType;
readonly EventType = EventType;
constructor() {
super();
}
get debugActionDisabled(): boolean {
return !debugCfActionEnabled(this.entity);
};
onDebugEventSelected(event: CalculatedFieldEventBody) {
(this.entitiesTableConfig as CalculatedFieldsTableConfig).getTestScriptDialog(this.entity, JSON.parse(event.arguments), false)
.subscribe((expression) => {
(this.entitiesTableConfig as CalculatedFieldsTableConfig).getTable();
const entityDetailsPanel = this.entitiesTableConfig.getTable().entityDetailsPanel;
entityDetailsPanel.onToggleEditMode(true);
entityDetailsPanel.selectedTab = 0;
setTimeout(() => {
entityDetailsPanel.detailsForm.get('configuration').setValue({...this.entity.configuration, expression});
entityDetailsPanel.detailsForm.get('configuration').markAsDirty();
});
});
};
}

4
ui-ngx/src/app/modules/home/pages/home-pages.module.ts

@ -27,7 +27,7 @@ import { UserModule } from '@modules/home/pages/user/user.module';
import { DeviceModule } from '@modules/home/pages/device/device.module'; import { DeviceModule } from '@modules/home/pages/device/device.module';
import { AssetModule } from '@modules/home/pages/asset/asset.module'; import { AssetModule } from '@modules/home/pages/asset/asset.module';
import { EntityViewModule } from '@modules/home/pages/entity-view/entity-view.module'; import { EntityViewModule } from '@modules/home/pages/entity-view/entity-view.module';
import { CalculatedFieldsModule } from '@home/pages/calculated-fields/calculated-fields.module'; import { CalculatedFieldPageModule } from '@home/pages/calculated-fields/calculated-field-page.module';
import { RuleChainModule } from '@modules/home/pages/rulechain/rulechain.module'; import { RuleChainModule } from '@modules/home/pages/rulechain/rulechain.module';
import { WidgetLibraryModule } from '@modules/home/pages/widget/widget-library.module'; import { WidgetLibraryModule } from '@modules/home/pages/widget/widget-library.module';
import { DashboardModule } from '@modules/home/pages/dashboard/dashboard.module'; import { DashboardModule } from '@modules/home/pages/dashboard/dashboard.module';
@ -70,7 +70,7 @@ import { AiModelModule } from '@home/pages/ai-model/ai-model.module';
EdgeModule, EdgeModule,
EntityViewModule, EntityViewModule,
CustomerModule, CustomerModule,
CalculatedFieldsModule, CalculatedFieldPageModule,
RuleChainModule, RuleChainModule,
WidgetLibraryModule, WidgetLibraryModule,
DashboardModule, DashboardModule,

8
ui-ngx/src/app/modules/login/pages/login/create-password.component.html

@ -21,9 +21,9 @@
<mat-card-title class="text-xl font-normal" translate>login.create-password</mat-card-title> <mat-card-title class="text-xl font-normal" translate>login.create-password</mat-card-title>
</mat-card-header> </mat-card-header>
<div class="h-3 px-4"><div tb-toast></div></div> <div class="h-3 px-4"><div tb-toast></div></div>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading"> <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading || (isLoading$ | async)">
</mat-progress-bar> </mat-progress-bar>
<span style="height: 4px;" *ngIf="!isLoading"></span> <span style="height: 4px;" *ngIf="!isLoading && !(isLoading$ | async)"></span>
<mat-card-content class="!p-6"> <mat-card-content class="!p-6">
<form [formGroup]="createPassword" (ngSubmit)="onCreatePassword()"> <form [formGroup]="createPassword" (ngSubmit)="onCreatePassword()">
<mat-form-field class="mat-block tb-autofilled" appearance="outline"> <mat-form-field class="mat-block tb-autofilled" appearance="outline">
@ -53,10 +53,10 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<div class="flex flex-col items-center justify-start gap-4 gt-xs:flex-row gt-xs:items-start gt-xs:justify-center"> <div class="flex flex-col items-center justify-start gap-4 gt-xs:flex-row gt-xs:items-start gt-xs:justify-center">
<button mat-flat-button color="accent" type="submit" [disabled]="isLoading"> <button mat-flat-button color="accent" type="submit" [disabled]="isLoading || (isLoading$ | async)">
{{ 'login.create-password' | translate }} {{ 'login.create-password' | translate }}
</button> </button>
<button mat-button type="button" [disabled]="isLoading" routerLink="/login"> <button mat-button type="button" [disabled]="isLoading || (isLoading$ | async)" routerLink="/login">
{{ 'action.cancel' | translate }} {{ 'action.cancel' | translate }}
</button> </button>
</div> </div>

15
ui-ngx/src/app/modules/login/pages/login/create-password.component.ts

@ -20,13 +20,15 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { UserPasswordPolicy } from '@shared/models/settings.models'; import { UserPasswordPolicy } from '@shared/models/settings.models';
import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/models/password.models'; import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/models/password.models';
import { finalize } from 'rxjs/operators';
import { PageComponent } from '@shared/components/page.component';
@Component({ @Component({
selector: 'tb-create-password', selector: 'tb-create-password',
templateUrl: './create-password.component.html', templateUrl: './create-password.component.html',
styleUrls: ['./password.component.scss'] styleUrls: ['./password.component.scss']
}) })
export class CreatePasswordComponent { export class CreatePasswordComponent extends PageComponent {
passwordPolicy: UserPasswordPolicy; passwordPolicy: UserPasswordPolicy;
createPassword: FormGroup; createPassword: FormGroup;
@ -38,7 +40,7 @@ export class CreatePasswordComponent {
constructor(private route: ActivatedRoute, constructor(private route: ActivatedRoute,
private authService: AuthService, private authService: AuthService,
private fb: FormBuilder) { private fb: FormBuilder) {
super();
this.activateToken = this.route.snapshot.queryParams['activateToken'] || ''; this.activateToken = this.route.snapshot.queryParams['activateToken'] || '';
this.passwordPolicy = this.route.snapshot.data['passwordPolicy']; this.passwordPolicy = this.route.snapshot.data['passwordPolicy'];
@ -60,11 +62,10 @@ export class CreatePasswordComponent {
if (this.createPassword.invalid) { if (this.createPassword.invalid) {
this.createPassword.markAllAsTouched(); this.createPassword.markAllAsTouched();
} else { } else {
this.isLoading = true this.isLoading = true;
this.authService.activate(this.activateToken, this.createPassword.get('newPassword').value, true) this.authService.activate(this.activateToken, this.createPassword.get('newPassword').value, true).pipe(
.subscribe({ finalize(() => {this.isLoading = false;})
error: () => {this.isLoading = false;} ).subscribe(() => {});
});
} }
} }
} }

6
ui-ngx/src/app/modules/login/pages/login/login.component.html

@ -22,9 +22,9 @@
<div class="flex flex-col items-center justify-start" style="padding: 16px 0;"> <div class="flex flex-col items-center justify-start" style="padding: 16px 0;">
<tb-logo link="https://thingsboard.io" target="_blank" class="login-logo"></tb-logo> <tb-logo link="https://thingsboard.io" target="_blank" class="login-logo"></tb-logo>
</div> </div>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading"> <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading || (isLoading$ | async)">
</mat-progress-bar> </mat-progress-bar>
<span style="height: 4px;" *ngIf="!isLoading"></span> <span style="height: 4px;" *ngIf="!isLoading && !(isLoading$ | async)"></span>
<span tb-toast style="height: 20px;"></span> <span tb-toast style="height: 20px;"></span>
<div class="flex flex-col"> <div class="flex flex-col">
@if(oauth2Clients?.length) { @if(oauth2Clients?.length) {
@ -70,7 +70,7 @@
<tb-toggle-password matSuffix></tb-toggle-password> <tb-toggle-password matSuffix></tb-toggle-password>
<mat-icon matPrefix>lock</mat-icon> <mat-icon matPrefix>lock</mat-icon>
</mat-form-field> </mat-form-field>
<button mat-flat-button color="accent" [disabled]="isLoading" <button mat-flat-button color="accent" [disabled]="isLoading || (isLoading$ | async)"
class="tb-action-button" type="submit">{{ 'login.sign-in' | translate }}</button> class="tb-action-button" type="submit">{{ 'login.sign-in' | translate }}</button>
<div class="forgot-password flex items-center justify-end gap-5"> <div class="forgot-password flex items-center justify-end gap-5">
<button class="tb-reset-password" mat-button type="button" routerLink="/login/resetPasswordRequest"> <button class="tb-reset-password" mat-button type="button" routerLink="/login/resetPasswordRequest">

11
ui-ngx/src/app/modules/login/pages/login/login.component.ts

@ -22,13 +22,15 @@ import { Constants } from '@shared/models/constants';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { OAuth2ClientLoginInfo } from '@shared/models/oauth2.models'; import { OAuth2ClientLoginInfo } from '@shared/models/oauth2.models';
import { validateEmail } from '@app/core/utils'; import { validateEmail } from '@app/core/utils';
import { PageComponent } from '@shared/components/page.component';
import { finalize } from 'rxjs/operators';
@Component({ @Component({
selector: 'tb-login', selector: 'tb-login',
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'] styleUrls: ['./login.component.scss']
}) })
export class LoginComponent implements OnInit { export class LoginComponent extends PageComponent implements OnInit {
passwordViolation = false; passwordViolation = false;
isLoading = false; isLoading = false;
@ -42,6 +44,7 @@ export class LoginComponent implements OnInit {
constructor(private authService: AuthService, constructor(private authService: AuthService,
public fb: UntypedFormBuilder, public fb: UntypedFormBuilder,
private router: Router) { private router: Router) {
super();
} }
ngOnInit() { ngOnInit() {
@ -51,10 +54,10 @@ export class LoginComponent implements OnInit {
login(): void { login(): void {
if (this.loginFormGroup.valid) { if (this.loginFormGroup.valid) {
this.isLoading = true; this.isLoading = true;
this.authService.login(this.loginFormGroup.value).subscribe({ this.authService.login(this.loginFormGroup.value).pipe(
next: () => {}, finalize(() => {this.isLoading = false;})
).subscribe({
error: (error: HttpErrorResponse) => { error: (error: HttpErrorResponse) => {
this.isLoading = false;
if (error && error.error && error.error.errorCode) { if (error && error.error && error.error.errorCode) {
if (error.error.errorCode === Constants.serverErrorCode.credentialsExpired) { if (error.error.errorCode === Constants.serverErrorCode.credentialsExpired) {
this.router.navigateByUrl(`login/resetExpiredPassword?resetToken=${error.error.resetToken}`); this.router.navigateByUrl(`login/resetExpiredPassword?resetToken=${error.error.resetToken}`);

8
ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts

@ -20,6 +20,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { UserPasswordPolicy } from '@shared/models/settings.models'; import { UserPasswordPolicy } from '@shared/models/settings.models';
import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/models/password.models'; import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/models/password.models';
import { finalize } from 'rxjs/operators';
@Component({ @Component({
selector: 'tb-reset-password', selector: 'tb-reset-password',
@ -64,10 +65,9 @@ export class ResetPasswordComponent {
this.resetPassword.markAllAsTouched(); this.resetPassword.markAllAsTouched();
} else { } else {
this.isLoading = true; this.isLoading = true;
this.authService.resetPassword(this.resetToken, this.resetPassword.get('newPassword').value).subscribe({ this.authService.resetPassword(this.resetToken, this.resetPassword.get('newPassword').value).pipe(
next: () => this.router.navigateByUrl('login'), finalize(() => {this.isLoading = false})
error: () => {this.isLoading = false;} ).subscribe(() => this.router.navigateByUrl('login'));
});
} }
} }
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save