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:
- 'Major Core'
- 'Major Rule Engine'
exclude:
labels:
- 'Bug'
- title: 'Major UI'
labels:
- 'Major UI'
exclude:
labels:
- 'Bug'
- title: 'Major Transport'
labels:
- 'Major Transport'
exclude:
labels:
- 'Bug'
- title: 'Major Edge'
labels:
- 'Major Edge'
exclude:
labels:
- 'Bug'
- title: 'Core & Rule Engine'
labels:
- 'Core'
@ -51,38 +43,63 @@ changelog:
exclude:
labels:
- 'Bug'
- title: 'UI'
labels:
- 'UI'
exclude:
labels:
- 'Bug'
- title: 'Transport'
labels:
- 'Transport'
exclude:
labels:
- 'Bug'
- title: 'Edge'
labels:
- 'Edge'
exclude:
labels:
- 'Bug'
- title: 'Bug: Core & Rule Engine'
labels:
- 'Core'
- 'Rule Engine'
- 'Bug'
exclude:
labels:
- 'UI'
- 'Transport'
- 'Edge'
- title: 'Bug: UI'
labels:
- 'UI'
- 'Bug'
exclude:
labels:
- 'Core'
- 'Rule Engine'
- 'Transport'
- 'Edge'
- title: 'Bug: Transport'
labels:
- 'Transport'
- 'Bug'
exclude:
labels:
- 'Core'
- 'Rule Engine'
- 'UI'
- 'Edge'
- title: 'Bug: Edge'
labels:
- 'Edge'
- '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) {
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) {
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<>();
if (!relatedEntityArgs.isEmpty() || !args.isEmpty()) {
for (TsKvProto item : data) {
ReferencedEntityKey key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_LATEST, null);
SingleValueArgumentEntry relatedArgIncoming = new SingleValueArgumentEntry(originator, item);
mapLatest(relatedArgIncoming, relatedEntityArgs.get(key), arguments);
mapLatest(ctx, relatedArgIncoming, relatedEntityArgs.get(key), arguments);
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);
mapRolling(item, args.get(key), arguments);
@ -598,7 +598,8 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
return arguments;
}
private void mapLatest(SingleValueArgumentEntry incoming,
private void mapLatest(CalculatedFieldCtx ctx,
SingleValueArgumentEntry incoming,
Set<String> argNames,
Map<String, ArgumentEntry> arguments) {
if (argNames != null) {
@ -606,7 +607,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (existing == null) {
return incoming;
}
existing.updateEntry(incoming);
existing.updateEntry(incoming, ctx);
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();
boolean updateEntry(ArgumentEntry entry);
boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx);
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);
if (existingEntry instanceof RelatedEntitiesArgumentEntry ||
existingEntry instanceof EntityAggregationArgumentEntry) {
updateEntry(existingEntry, newEntry);
updateEntry(existingEntry, newEntry, ctx);
} else {
arguments.put(key, newEntry);
}
entryUpdated = true;
} else {
entryUpdated = updateEntry(existingEntry, newEntry);
entryUpdated = updateEntry(existingEntry, newEntry, ctx);
}
if (entryUpdated) {
@ -111,8 +111,8 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState,
return updatedArguments;
}
protected boolean updateEntry(ArgumentEntry existingEntry, ArgumentEntry newEntry) {
return existingEntry.updateEntry(newEntry);
protected boolean updateEntry(ArgumentEntry existingEntry, ArgumentEntry newEntry, CalculatedFieldCtx ctx) {
return existingEntry.updateEntry(newEntry, ctx);
}
@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
public boolean updateEntry(ArgumentEntry entry) {
public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) {
if (entry instanceof SingleValueArgumentEntry singleValueEntry) {
if (singleValueEntry.getTs() < this.ts) {
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
public boolean updateEntry(ArgumentEntry entry) {
public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) {
if (entry instanceof TsRollingArgumentEntry tsRollingEntry) {
updateTsRollingEntry(tsRollingEntry);
} 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.service.cf.ctx.state.ArgumentEntry;
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.SingleValueArgumentEntry;
@ -63,7 +64,7 @@ public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs
}
@Override
public boolean updateEntry(ArgumentEntry entry) {
public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) {
if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) {
entityInputs.putAll(relatedEntitiesArgumentEntry.entityInputs);
return true;
@ -74,7 +75,7 @@ public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs
}
ArgumentEntry argumentEntry = entityInputs.get(singleValueArgumentEntry.getEntityId());
if (argumentEntry != null) {
argumentEntry.updateEntry(singleValueArgumentEntry);
argumentEntry.updateEntry(singleValueArgumentEntry, ctx);
} else {
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 org.thingsboard.common.util.JacksonUtil;
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.ArgumentEntryType;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Data
public class EntityAggregationArgumentEntry implements ArgumentEntry {
@ -47,28 +54,63 @@ public class EntityAggregationArgumentEntry implements ArgumentEntry {
}
@Override
public boolean updateEntry(ArgumentEntry entry) {
boolean updated = false;
public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) {
if (entry instanceof EntityAggregationArgumentEntry entityAggEntry) {
aggIntervals.putAll(entityAggEntry.getAggIntervals());
return true;
} else if (entry instanceof SingleValueArgumentEntry singleValueArgEntry) {
long entryTs = singleValueArgEntry.getTs();
long argUpdateTs = System.currentTimeMillis();
for (Map.Entry<AggIntervalEntry, AggIntervalEntryStatus> aggIntervalEntry : aggIntervals.entrySet()) {
if (singleValueArgEntry.isForceResetPrevious()) {
aggIntervalEntry.getValue().setLastArgsRefreshTs(argUpdateTs);
updated = true;
continue;
}
if (aggIntervalEntry.getKey().belongsToInterval(entryTs)) {
aggIntervalEntry.getValue().setLastArgsRefreshTs(argUpdateTs);
return true;
}
long now = System.currentTimeMillis();
if (updateExistingIntervals(singleValueArgEntry, entryTs, now)) {
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;
}
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
public boolean 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() {
long now = System.currentTimeMillis();
ZoneId zoneId = interval.getZoneId();
long currentIntervalEndTs = interval.getCurrentIntervalEndTs();
long watermarkThresholdTs = now - watermarkDuration;
Map<AggIntervalEntry, Map<String, AggIntervalEntryStatus>> intervals = getIntervals();
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) {
long nextStartTs = nextStart.toInstant().toEpochMilli();
long nextEndTs = nextEnd.toInstant().toEpochMilli();
if (nextEndTs < watermarkThresholdTs) {
nextStart = nextEnd;
nextEnd = interval.getNextIntervalStart(nextStart);
continue;
}
AggIntervalEntry missing = new AggIntervalEntry(nextStartTs, nextEndTs);
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;
}
}
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()) {
initCurrentAlarm(ctx);
@ -224,17 +225,17 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
}
@Override
protected boolean updateEntry(ArgumentEntry existingArgumentEntry, ArgumentEntry newArgumentEntry) {
protected boolean updateEntry(ArgumentEntry existingArgumentEntry, ArgumentEntry newArgumentEntry, CalculatedFieldCtx ctx) {
if (!(existingArgumentEntry instanceof SingleValueArgumentEntry existingEntry) ||
!(newArgumentEntry instanceof SingleValueArgumentEntry newEntry)) {
return super.updateEntry(existingArgumentEntry, newArgumentEntry);
return super.updateEntry(existingArgumentEntry, newArgumentEntry, ctx);
}
if (newEntry.getTs() < existingEntry.getTs()) {
if (existingEntry.isDefaultValue()) {
existingEntry.setTs(newEntry.getTs());
}
}
return super.updateEntry(existingEntry, newEntry);
return super.updateEntry(existingEntry, newEntry, ctx);
}
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() {
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.service.cf.ctx.state.ArgumentEntry;
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 java.util.Map;
@ -68,7 +69,7 @@ public class GeofencingArgumentEntry implements ArgumentEntry, HasLatestTs {
}
@Override
public boolean updateEntry(ArgumentEntry entry) {
public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) {
if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
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.service.cf.ctx.state.ArgumentEntry;
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.Collection;
@ -59,7 +60,7 @@ public class PropagationArgumentEntry implements ArgumentEntry {
}
@Override
public boolean updateEntry(ArgumentEntry entry) {
public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) {
if (!(entry instanceof PropagationArgumentEntry updated)) {
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 org.thingsboard.common.util.JacksonUtil;
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.rest.TbSendRestApiCallReplyNode;
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(
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,
Set.of(
TbCalculatedFieldsNode.class.getName()
@ -560,7 +569,7 @@ public class EdgeMsgConstructorUtils {
.setEntityIdMSB(entityId.getId().getMostSignificantBits())
.setEntityIdLSB(entityId.getId().getLeastSignificantBits())
.setEntityType(entityId.getEntityType().name());
long ts = getTs(entityData.getAsJsonObject());
long ts = extractTs(entityData.getAsJsonObject());
switch (actionType) {
case TIMESERIES_UPDATED:
try {
@ -613,8 +622,8 @@ public class EdgeMsgConstructorUtils {
return builder.build();
}
private static long getTs(JsonObject data) {
if (data.get("ts") != null && !data.get("ts").isJsonNull()) {
private static long extractTs(JsonObject data) {
if (data.has("ts") && data.get("ts").isJsonPrimitive()) {
return data.getAsJsonPrimitive("ts").getAsLong();
}
return System.currentTimeMillis();
@ -740,7 +749,7 @@ public class EdgeMsgConstructorUtils {
result.sort(Comparator.comparingLong(EdgeEvent::getSeqId));
return result;
} 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;
}
}
@ -751,6 +760,9 @@ public class EdgeMsgConstructorUtils {
}
String bodyStr = JacksonUtil.toString(body);
var jsonObject = JsonParser.parseString(bodyStr).getAsJsonObject();
if (!jsonObject.has("ts")) {
return new AttrsTs(0L, List.of());
}
long ts = jsonObject.get("ts").getAsLong();
var kv = jsonObject.getAsJsonObject("kv");
List<AttributeKvEntry> attrs = JsonConverter.convertToAttributes(
@ -761,22 +773,24 @@ public class EdgeMsgConstructorUtils {
}
private static JsonNode filterAttributesBody(JsonNode body, Map<String, Long> latestByKey) {
if (body == null || latestByKey == null || latestByKey.isEmpty()) {
if (body == null) {
return null;
}
String bodyStr = JacksonUtil.toString(body);
JsonObject jsonObject = JsonParser.parseString(bodyStr).getAsJsonObject();
long ts = jsonObject.get("ts").getAsLong();
JsonObject kv = jsonObject.getAsJsonObject("kv");
for (Iterator<Map.Entry<String, JsonElement>> it = kv.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, JsonElement> e = it.next();
Long latestTs = latestByKey.get(e.getKey());
if (latestTs == null || !latestTs.equals(ts)) {
it.remove();
if (jsonObject.has("ts") && latestByKey != null && !latestByKey.isEmpty()) {
long ts = jsonObject.get("ts").getAsLong();
JsonObject kv = jsonObject.getAsJsonObject("kv");
for (Iterator<Map.Entry<String, JsonElement>> it = kv.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, JsonElement> e = it.next();
Long latestTs = latestByKey.get(e.getKey());
if (latestTs == null || !latestTs.equals(ts)) {
it.remove();
}
}
if (kv.isEmpty()) {
return null;
}
}
if (kv.isEmpty()) {
return null;
}
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.).
// 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 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}"
gateway:
# 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:

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)
public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest {
private final String TZ = "Europe/Kyiv";
private Tenant savedTenant;
@Before
@ -93,7 +95,7 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
public void testCreateCfAndNoTelemetryDuringInterval_checkAggregation() throws Exception {
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);
long interval = customInterval.getCurrentIntervalDurationMillis();
@ -113,7 +115,7 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
public void testCreateCfWithoutWatermark_checkAggregation() throws Exception {
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);
long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs();
@ -156,7 +158,7 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
public void testCreateCfWithWatermark_checkAggregationDuringWatermark() throws Exception {
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);
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) {
Map<String, Argument> arguments = new HashMap<>();
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 coapDeviceProfileId;
@Value("${device.connectivity.gateway.image_version:3.7-stable}")
@Value("${device.connectivity.gateway.image_version:3.8-stable}")
private String gatewayImageVersion;
@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.EntityId;
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.DeviceCredentialsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
@ -671,8 +669,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
.atMost(10, TimeUnit.SECONDS)
.until(() -> {
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);
});

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 org.junit.jupiter.api.BeforeEach;
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.server.common.data.id.AssetId;
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.assertThatThrownBy;
@ExtendWith(MockitoExtension.class)
public class GeofencingValueArgumentEntryTest {
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;
@Mock
private CalculatedFieldCtx ctx;
@BeforeEach
void setUp() {
entry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry, ZONE_2_ID, restrictedZoneAttributeKvEntry));
@ -58,14 +65,14 @@ public class GeofencingValueArgumentEntryTest {
@Test
void testUpdateEntryWhenSingleEntryPassed() {
assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry()))
assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry(), ctx))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported argument entry type for geofencing argument entry: SINGLE_VALUE");
}
@Test
void testUpdateEntryWhenRollingEntryPassed() {
assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L)))
assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L), ctx))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported argument entry type for geofencing argument entry: TS_ROLLING");
}
@ -74,7 +81,7 @@ public class GeofencingValueArgumentEntryTest {
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);
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
@ -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);
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);
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);
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);
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);
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
@ -134,7 +141,7 @@ public class GeofencingValueArgumentEntryTest {
BaseAttributeKvEntry newTsAndTheSameValue = new BaseAttributeKvEntry(allowedZoneDataEntry, 364L, 156L);
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
@ -142,7 +149,7 @@ public class GeofencingValueArgumentEntryTest {
BaseAttributeKvEntry oldTsAndTheSameValue = new BaseAttributeKvEntry(allowedZoneDataEntry, 362L, 156L);
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
@ -150,7 +157,7 @@ public class GeofencingValueArgumentEntryTest {
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);
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

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.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.TbelCfPropagationArg;
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.assertThatThrownBy;
@ExtendWith(MockitoExtension.class)
public class PropagationArgumentEntryTest {
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;
@Mock
private CalculatedFieldCtx ctx;
@BeforeEach
void setUp() {
List<EntityId> propagationEntityIds = new ArrayList<>();
@ -68,14 +75,14 @@ public class PropagationArgumentEntryTest {
@Test
void testUpdateEntryWhenSingleEntryPassed() {
assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry()))
assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry(), ctx))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported argument entry type for propagation argument entry: SINGLE_VALUE");
}
@Test
void testUpdateEntryWhenRollingEntryPassed() {
assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L)))
assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L), ctx))
.isInstanceOf(IllegalArgumentException.class)
.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 updated = new PropagationArgumentEntry(newIds);
boolean changed = entry.updateEntry(updated);
boolean changed = entry.updateEntry(updated, ctx);
assertThat(changed).isTrue();
assertThat(entry.getEntityIds()).containsExactlyElementsOf(newIds);
@ -95,7 +102,7 @@ public class PropagationArgumentEntryTest {
void testUpdateEntryClearsWhenNewEntryIsEmpty() {
var updatedEmpty = new PropagationArgumentEntry(List.of());
boolean changed = entry.updateEntry(updatedEmpty);
boolean changed = entry.updateEntry(updatedEmpty, ctx);
assertThat(changed).isTrue();
assertThat(entry.getEntityIds()).isEmpty();
@ -106,7 +113,7 @@ public class PropagationArgumentEntryTest {
var added = new PropagationArgumentEntry();
added.setAdded(List.of(ENTITY_3_ID));
boolean changed = entry.updateEntry(added);
boolean changed = entry.updateEntry(added, ctx);
assertThat(changed).isTrue();
assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID);
@ -118,7 +125,7 @@ public class PropagationArgumentEntryTest {
var added = new PropagationArgumentEntry();
added.setAdded(List.of(ENTITY_2_ID));
boolean changed = entry.updateEntry(added);
boolean changed = entry.updateEntry(added, ctx);
assertThat(changed).isFalse();
assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID);
@ -130,7 +137,7 @@ public class PropagationArgumentEntryTest {
var removed = new PropagationArgumentEntry();
removed.setRemoved(ENTITY_2_ID);
boolean changed = entry.updateEntry(removed);
boolean changed = entry.updateEntry(removed, ctx);
assertThat(changed).isTrue();
assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID);
@ -142,7 +149,7 @@ public class PropagationArgumentEntryTest {
var removed = new PropagationArgumentEntry();
removed.setRemoved(ENTITY_3_ID);
boolean changed = entry.updateEntry(removed);
boolean changed = entry.updateEntry(removed, ctx);
assertThat(changed).isFalse();
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));
restore.setIgnoreRemovedEntities(true);
boolean changed = entry.updateEntry(restore);
boolean changed = entry.updateEntry(restore, ctx);
assertThat(changed).isTrue();
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));
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(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));
restore.setIgnoreRemovedEntities(true);
boolean changed = entry.updateEntry(restore);
boolean changed = entry.updateEntry(restore, ctx);
assertThat(changed).isTrue();
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));
restore.setIgnoreRemovedEntities(true);
boolean changed = entry.updateEntry(restore);
boolean changed = entry.updateEntry(restore, ctx);
assertThat(changed).isFalse();
assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID);
@ -211,7 +218,7 @@ public class PropagationArgumentEntryTest {
var restore = new PropagationArgumentEntry(List.of());
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(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.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.EntityId;
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.assertThatThrownBy;
@ExtendWith(MockitoExtension.class)
public class RelatedEntitiesArgumentEntryTest {
private RelatedEntitiesArgumentEntry entry;
@Mock
private CalculatedFieldCtx ctx;
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"));
@ -50,7 +57,7 @@ public class RelatedEntitiesArgumentEntryTest {
@Test
void testUpdateEntryWhenNotAggEntryPassed() {
assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L)))
assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L), ctx))
.isInstanceOf(IllegalArgumentException.class)
.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))
), false);
assertThat(entry.updateEntry(relatedEntitiesArgumentEntry)).isTrue();
assertThat(entry.updateEntry(relatedEntitiesArgumentEntry, ctx)).isTrue();
Map<EntityId, ArgumentEntry> aggInputs = entry.getEntityInputs();
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));
assertThat(entry.updateEntry(singleEntityArgumentEntry)).isTrue();
assertThat(entry.updateEntry(singleEntityArgumentEntry, ctx)).isTrue();
Map<EntityId, ArgumentEntry> aggInputs = entry.getEntityInputs();
assertThat(aggInputs.size()).isEqualTo(3);
@ -90,7 +97,7 @@ public class RelatedEntitiesArgumentEntryTest {
void testUpdateEntryWhenSingleValueArgumentEntryPassedAndEntryByIdExist() {
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();
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.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.TbelCfSingleValueArg;
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.assertThatThrownBy;
@ExtendWith(MockitoExtension.class)
public class SingleValueArgumentEntryTest {
private SingleValueArgumentEntry entry;
@Mock
private CalculatedFieldCtx ctx;
private final long ts = System.currentTimeMillis();
@BeforeEach
@ -47,48 +54,48 @@ public class SingleValueArgumentEntryTest {
@Test
void testUpdateEntryWhenRollingEntryPassed() {
assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L)))
assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L), ctx))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported argument entry type for single value argument entry: " + ArgumentEntryType.TS_ROLLING);
}
@Test
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
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
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.getVersion()).isNull();
}
@Test
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.getVersion()).isEqualTo(369L);
}
@Test
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
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
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

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.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.StringDataEntry;
@ -26,10 +29,14 @@ import java.util.TreeMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ExtendWith(MockitoExtension.class)
public class TsRollingArgumentEntryTest {
private TsRollingArgumentEntry entry;
@Mock
private CalculatedFieldCtx ctx;
private final long ts = System.currentTimeMillis();
@BeforeEach
@ -51,7 +58,7 @@ public class TsRollingArgumentEntryTest {
void testUpdateEntryWhenSingleValueEntryPassed() {
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().get(ts - 10)).isEqualTo(23.0);
}
@ -64,7 +71,7 @@ public class TsRollingArgumentEntryTest {
values.put(ts - 5, 1.0);
newEntry.setTsRecords(values);
assertThat(entry.updateEntry(newEntry)).isTrue();
assertThat(entry.updateEntry(newEntry, ctx)).isTrue();
assertThat(entry.getTsRecords()).hasSize(5);
assertThat(entry.getTsRecords()).isEqualTo(Map.of(
ts - 40, 10.0,
@ -79,7 +86,7 @@ public class TsRollingArgumentEntryTest {
void testUpdateEntryWhenValueIsNotNumber() {
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();
}
@ -93,7 +100,7 @@ public class TsRollingArgumentEntryTest {
newEntry.setTsRecords(values);
entry = new TsRollingArgumentEntry(3, 30000L);
assertThat(entry.updateEntry(newEntry)).isTrue();
assertThat(entry.updateEntry(newEntry, ctx)).isTrue();
assertThat(entry.getTsRecords()).hasSize(1);
assertThat(entry.getTsRecords()).isEqualTo(Map.of(
ts - 5, 0.0
@ -111,7 +118,7 @@ public class TsRollingArgumentEntryTest {
newEntry.setTsRecords(values);
entry = new TsRollingArgumentEntry(3, 30000L);
assertThat(entry.updateEntry(newEntry)).isTrue();
assertThat(entry.updateEntry(newEntry, ctx)).isTrue();
assertThat(entry.getTsRecords()).hasSize(3);
assertThat(entry.getTsRecords()).isEqualTo(Map.of(
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"));
}
@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) {
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) {
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
public String getObjectIdByKeyNameFromProfile(LwM2mClient client, String keyName) {
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()) {
String k = entry.getKey();
String v = entry.getValue();
@ -347,6 +348,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
PowerMode powerMode = lwM2MClient.getPowerMode();
if (powerMode == null) {
Lwm2mDeviceProfileTransportConfiguration deviceProfile = getProfile(lwM2MClient.getRegistration());
if (deviceProfile == null) return null;
powerMode = deviceProfile.getClientLwM2mSettings().getPowerMode();
}
return powerMode;
@ -377,8 +379,8 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
}
}
} else {
log.warn("Device profile not found! The device profile ID is null. Return Lwm2mDeviceProfileTransportConfiguration with default.");
result = new Lwm2mDeviceProfileTransportConfiguration();
log.warn("Device profile not found! The device profile ID is null. Return Lwm2mDeviceProfileTransportConfiguration with null.");
result = null;
}
return result;
}
@ -413,6 +415,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
OtherConfiguration profileSettings = null;
if (powerMode == null && client.getProfileId() != null) {
var clientProfile = getProfile(client.getRegistration());
if (clientProfile == null) return true;
profileSettings = clientProfile.getClientLwM2mSettings();
powerMode = profileSettings.getPowerMode();
}
@ -459,8 +462,10 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
OtherConfiguration profileSettings = null;
if (powerMode == null && client.getProfileId() != null) {
var clientProfile = getProfile(client.getRegistration());
profileSettings = clientProfile.getClientLwM2mSettings();
powerMode = profileSettings.getPowerMode();
if (clientProfile != null) {
profileSettings = clientProfile.getClientLwM2mSettings();
powerMode = profileSettings.getPowerMode();
}
}
if (powerMode == null || PowerMode.DRX.equals(powerMode)) {
client.updateLastUplinkTime();
@ -515,9 +520,11 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
timeout = client.getEdrxCycle();
} else {
var clientProfile = getProfile(client.getRegistration());
OtherConfiguration clientLwM2mSettings = clientProfile.getClientLwM2mSettings();
if (PowerMode.E_DRX.equals(clientLwM2mSettings.getPowerMode())) {
timeout = clientLwM2mSettings.getEdrxCycle();
if (clientProfile != null) {
OtherConfiguration clientLwM2mSettings = clientProfile.getClientLwM2mSettings();
if (PowerMode.E_DRX.equals(clientLwM2mSettings.getPowerMode())) {
timeout = clientLwM2mSettings.getEdrxCycle();
}
}
}
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);
}
var clientSettings = clientContext.getProfile(client.getRegistration()).getClientLwM2mSettings();
var clientProfile = clientContext.getProfile(client.getRegistration());
var clientSettings = clientProfile != null ? clientProfile.getClientLwM2mSettings() : null;
if (clientSettings != null) {
initFwStrategy(client, clientSettings);
initSwStrategy(client, clientSettings);
@ -532,7 +533,8 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
} else {
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){
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())) {
UUID otaPackageId = new UUID(response.getOtaPackageIdMSB(), response.getOtaPackageIdLSB());
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){
sendInfoToObject19ForOta(client, SW_INFO_19_INSTANCE_ID, response, otaPackageId);
}
@ -647,8 +650,9 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
LwM2MClientSwOtaInfo info = otaInfoStore.getSw(endpoint);
if (info == null) {
var profile = clientContext.getProfile(client.getRegistration());
info = new LwM2MClientSwOtaInfo(endpoint, profile.getClientLwM2mSettings().getSwUpdateResource(),
LwM2MSoftwareUpdateStrategy.fromStrategySwByCode(profile.getClientLwM2mSettings().getSwUpdateStrategy()));
OtherConfiguration clientLwM2mSettings = profile == null ? new OtherConfiguration() : profile.getClientLwM2mSettings();
info = new LwM2MClientSwOtaInfo(endpoint, clientLwM2mSettings.getSwUpdateResource(),
LwM2MSoftwareUpdateStrategy.fromStrategySwByCode(clientLwM2mSettings.getSwUpdateStrategy()));
update(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()) {
var oldProfile = clientContext.getProfile(clients.get(0).getRegistration());
this.onDeviceProfileUpdate(clients, oldProfile, deviceProfile);
if (oldProfile != null) this.onDeviceProfileUpdate(clients, oldProfile, deviceProfile);
}
} catch (Exception e) {
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) {
var oldProfile = clientContext.getProfile(lwM2MClient.getRegistration());
deviceProfileOpt.ifPresent(deviceProfile -> this.onDeviceProfileUpdate(Collections.singletonList(lwM2MClient), oldProfile, deviceProfile));
lwM2MClient.onDeviceUpdate(device, deviceProfileOpt);
if (oldProfile != null) {
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();
ResultsAddKeyValueProto results = new ResultsAddKeyValueProto();
var profile = clientContext.getProfile(registration);
List<TransportProtos.KeyValueProto> resultAttributes = new ArrayList<>();
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);
if (profile != null) {
List<TransportProtos.KeyValueProto> resultAttributes = new ArrayList<>();
Set<String> attributes = profile.getObserveAttr().getAttribute().stream()
.map(LwM2MTransportUtil::fromVersionedIdToObjectId)
.filter(path::contains)
.filter(paths::contains)
.collect(Collectors.toSet());
if (!attributes.isEmpty()){
if (!attributes.isEmpty()) {
attributes.stream()
.map(attr -> this.getKvToThingsBoard(attr, registration))
.filter(Objects::nonNull)
@ -786,10 +755,9 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
}
List<TransportProtos.KeyValueProto> resultTelemetries = new ArrayList<>();
Set<String> telemetries = profile.getObserveAttr().getTelemetry().stream()
.map(LwM2MTransportUtil::fromVersionedIdToObjectId)
.filter(path::contains)
.filter(paths::contains)
.collect(Collectors.toSet());
if (!telemetries.isEmpty()){
if (!telemetries.isEmpty()) {
telemetries.stream()
.map(telemetry -> this.getKvToThingsBoard(telemetry, registration))
.filter(Objects::nonNull)
@ -801,14 +769,15 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
if (resultTelemetries.size() > 0) {
results.setResultTelemetries(resultTelemetries);
}
return results;
}
return null;
return results;
}
private TransportProtos.KeyValueProto getKvToThingsBoard(String pathIdVer, Registration registration) {
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)) {
String resourceName = names.get(pathIdVer);
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) {
if (clientContext.profileUpdate(deviceProfile) != null) {
var newProfileTransportConfiguration = clientContext.getProfile(clients.get(0).getRegistration());
ParametersUpdateAnalyzeResult parametersUpdate = getParametersUpdate(oldProfileTransportConfiguration, newProfileTransportConfiguration);
ParametersObserveAnalyzeResult parametersObserve = getParametersObserve(oldProfileTransportConfiguration.getObserveAttr(), newProfileTransportConfiguration.getObserveAttr(), deviceProfile.getId().getId());
compareAndSetWriteAttributesObservations(clients, parametersUpdate, parametersObserve);
updateValueOta(clients, newProfileTransportConfiguration, oldProfileTransportConfiguration);
if (newProfileTransportConfiguration != null) {
ParametersUpdateAnalyzeResult parametersUpdate = getParametersUpdate(oldProfileTransportConfiguration, newProfileTransportConfiguration);
ParametersObserveAnalyzeResult parametersObserve = getParametersObserve(oldProfileTransportConfiguration.getObserveAttr(), newProfileTransportConfiguration.getObserveAttr(), deviceProfile.getId().getId());
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;
@Value("${device.connectivity.coaps.pem_cert_file:}")
private String coapsPemCertFile;
@Value("${device.connectivity.gateway.image_version:3.7-stable}")
@Value("${device.connectivity.gateway.image_version:3.8-stable}")
private String gatewayImageVersion;
@Override

86
msa/pom.xml

@ -125,6 +125,92 @@
</plugins>
</build>
</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>
<build>
<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);
}
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();
cssParser.testMode = false;
const cssClass = `${cssClassPrefix}-${guid()}`;
cssParser.cssPreviewNamespace = cssClass;
cssParser.cssPreviewNamespace = addTbDefaultClass ? 'tb-default .' + cssClass : cssClass;
cssParser.createStyleElement(cssClass, css);
renderer.addClass(element, cssClass);
return cssClass;

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

@ -58,31 +58,15 @@
/>
</div>
@if (!data.entityId) {
<div class="flex flex-row gap-4 xs:flex-col" formGroupName="entityId">
<tb-entity-type-select #entityTypeSelect
appearance="outline"
subscriptSizing="dynamic"
class="flex-1"
showLabel
required
label="{{ 'entity.entity-type' | translate }}"
[filterAllowedEntityTypes]="false"
[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>
<tb-entity-select formControlName="entityId"
appearance="outline"
required
entityTypeLabel="{{ 'entity.entity-type' | translate }}"
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="alarmRuleEntityTypeList"
[defaultEntityType]="EntityType.DEVICE_PROFILE"
(entityChanged)="changeEntity($event)">
</tb-entity-select>
}
</div>
<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.
///
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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
import { CalculatedField, CalculatedFieldArgument, CalculatedFieldType } from '@shared/models/calculated-field.models';
import { oneSpaceInsideRegex } from '@shared/models/regex.constants';
import { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from '@shared/models/rule-node.models';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
@ -39,13 +38,11 @@ import {
} from "@shared/models/alarm-rule.models";
import { deepTrim } from "@core/utils";
import { combineLatest, Observable } from "rxjs";
import { debounceTime, startWith, switchMap } 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 { debounceTime, startWith } from "rxjs/operators";
import { RelationTypes } from "@shared/models/relation.models";
import { StringItemsOption } from "@shared/components/string-items-list.component";
import { BaseData } from "@shared/models/base-data";
import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service';
export interface AlarmRuleDialogData {
value?: CalculatedField;
@ -67,24 +64,7 @@ export interface AlarmRuleDialogData {
})
export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogComponent, CalculatedField> {
fieldFormGroup = this.fb.group({
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],
}),
});
fieldFormGroup: FormGroup ;
additionalDebugActionConfig = this.data.value?.id ? {
...this.data.additionalDebugActionConfig,
@ -105,18 +85,15 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
disabledArguments = false;
isLoading = false;
@ViewChild('entityTypeSelect') entityTypeSelect: EntityTypeSelectComponent;
@ViewChild('entityAutocompleteComponent') entityAutocompleteComponent: EntityAutocompleteComponent;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData,
protected dialogRef: MatDialogRef<AlarmRuleDialogComponent, CalculatedField>,
private calculatedFieldsService: CalculatedFieldsService,
private entityService: EntityService,
private destroyRef: DestroyRef,
private fb: FormBuilder) {
private cfFormService: CalculatedFieldFormService) {
super(store, router, dialogRef);
this.fieldFormGroup = this.cfFormService.buildAlarmRuleForm();
this.applyDialogData();
this.updateRulesValidators();
@ -132,7 +109,7 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
if (!this.data.entityId) {
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))
]).pipe(
debounceTime(50),
@ -196,8 +173,6 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
});
} else {
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> {
const calculatedFieldId = this.data.value?.id?.id;
if (calculatedFieldId) {
return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true})
.pipe(
switchMap(event => {
const args = event?.arguments ? JSON.parse(event.arguments) : null;
return this.data.getTestScriptDialogFn(this.fromGroupValue, expression, args, false);
}),
takeUntilDestroyed(this.destroyRef)
)
}
return this.data.getTestScriptDialogFn(this.fromGroupValue, expression, null, false);
return this.cfFormService.testScript(
this.data.value?.id?.id,
this.fromGroupValue,
this.data.getTestScriptDialogFn,
this.destroyRef,
expression
);
}
private updateRulesValidators(): void {
@ -250,5 +220,4 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
changeEntity(entity: BaseData<EntityId>): void {
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() {
this.updateAlarmRuleConfigForm(this.alarmRuleFilterConfig);
this.alarmRuleFilterConfigForm.markAsPristine();
if (this.overlayRef) {
this.overlayRef.dispose();
} 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 {
AlarmRuleFilterPredicateNoDataValueComponent
} 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({
declarations: [
@ -73,7 +74,8 @@ import {
AlarmRuleDetailsDialogComponent,
AlarmRuleFilterConfigComponent,
AlarmRuleTableHeaderComponent,
AlarmRuleFilterPredicateNoDataValueComponent
AlarmRuleFilterPredicateNoDataValueComponent,
AlarmRulesComponent
],
imports: [
CommonModule,
@ -83,6 +85,7 @@ import {
],
exports: [
AlarmRuleDialogComponent,
AlarmRulesComponent
]
})
export class AlarmRuleModule { }

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

@ -21,7 +21,7 @@ import {
EntityTableColumn,
EntityTableConfig
} 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 { Direction } from '@shared/models/page/sort-order';
import { MatDialog } from '@angular/material/dialog';
@ -36,7 +36,7 @@ import { DestroyRef, Renderer2 } from '@angular/core';
import { EntityDebugSettings } from '@shared/models/entity.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
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 {
ArgumentEntityType,
ArgumentType,
@ -67,6 +67,10 @@ import {
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestScriptDialogData
} 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;
@ -75,7 +79,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
readonly tenantId = getCurrentAuthUser(this.store).tenantId;
additionalDebugActionConfig = {
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;
@ -93,27 +97,26 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService,
private router: Router,
public pageMode: boolean = false,
) {
super();
if (this.pageMode) {
this.headerComponent = AlarmRuleTableHeaderComponent;
this.entityComponent = AlarmRulesComponent;
this.entityTabsComponent = AlarmRulesTabsComponent;
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.detailsPanelEnabled = false;
this.detailsPanelEnabled = this.pageMode;
this.entityResources = entityTypeResources.get(EntityType.CALCULATED_FIELD);
this.entityType = EntityType.CALCULATED_FIELD;
this.entityTranslations = {
type: 'alarm-rule.alarm-rule',
typePlural: 'alarm-rule.alarm-rules',
list: 'alarm-rule.list',
add: 'action.add',
details: 'alarm-rule.details',
noEntities: 'alarm-rule.no-found',
search: 'action.search',
selectedEntities: 'alarm-rule.selected-fields'
@ -121,11 +124,17 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink);
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.deleteEntityContent = () => this.translate.instant('alarm-rule.delete-text');
this.deleteEntitiesTitle = count => this.translate.instant('alarm-rule.delete-multiple-title', {count});
this.deleteEntitiesContent = () => this.translate.instant('alarm-rule.delete-multiple-text');
this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id);
this.onEntityAction = action => this.onCFAction(action);
this.addActionDescriptors = [
{
name: this.translate.instant('alarm-rule.create'),
@ -177,7 +186,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
name: this.translate.instant('entity-view.events'),
icon: 'mdi:clipboard-text-clock',
isEnabled: () => true,
onAction: ($event, entity) => this.openDebugEventsDialog($event, entity),
onAction: ($event, entity) =>
this.pageMode ? this.openDebugTab($event, entity) : this.openDebugEventsDialog($event, entity),
},
{
name: '',
@ -186,14 +196,18 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
isEnabled: () => true,
iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline',
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>> {
@ -288,13 +302,29 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
debugEventTypes:[DebugEventType.DEBUG_CALCULATED_FIELD],
disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS],
defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD,
debugActionEnabledFn: () => false
debugActionDisabled: true
}
})
.afterClosed()
.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 {
$event?.stopPropagation();
this.importExportService.exportCalculatedField(calculatedField.id.id);
@ -348,7 +378,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
).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) {
const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
const type = calculatedField.configuration.arguments[key].refEntityKey.type;
@ -384,4 +414,22 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
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 { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config";
import { UtilsService } from "@core/services/utils.service";
import { ActivatedRoute } from "@angular/router";
import { ActivatedRoute, Router } from "@angular/router";
@Component({
selector: 'tb-alarm-rules-table',
@ -70,6 +70,7 @@ export class AlarmRulesTableComponent {
private utilsService: UtilsService,
private destroyRef: DestroyRef,
private route: ActivatedRoute,
private router: Router
) {
this.pageMode = !!this.route.snapshot.data.isPage;
effect(() => {
@ -88,6 +89,7 @@ export class AlarmRulesTableComponent {
this.importExportService,
this.entityDebugSettingsService,
this.utilsService,
this.router,
this.pageMode,
);
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.
-->
<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="min-w-40 xs:min-w-fit">{{ 'alarm-rule.condition' | translate }}</div>
<button [disabled]="disabled"
@ -23,7 +23,7 @@
class="tb-alarm-rule-condition-button"
mat-stroked-button [color]="conditionSet() && this.filtersArgumentsValid ? 'primary' : 'warn'"
(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"
[arguments]="arguments"
[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 {
this.modelValue = value;
this.updateConditionInfo();
if (value) {
this.onValidatorChange();
if (value && !this.disabled) {
if (this.validate(this.alarmRuleConditionFormGroup)) {
this.onValidatorChange();
}
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes.arguments) {
if (changes.arguments && !changes.arguments.firstChange && this.modelValue) {
this.onValidatorChange();
if (changes.arguments && !changes.arguments.firstChange && this.modelValue && !this.disabled) {
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.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments));
this.onValidatorChange = () => {
control.updateValueAndValidity({ emitEvent: true });
control.updateValueAndValidity({ emitEvent: !this.disabled });
};
return this.conditionSet() && this.filtersArgumentsValid && this.schedulerArgumentsValid ? null : {
alarmRuleCondition: {
@ -303,7 +307,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
}
private updateScheduleText() {
let schedule = this.modelValue?.schedule;
const schedule = this.modelValue?.schedule;
this.scheduleText = '';
if (isDefinedAndNotNull(schedule)) {
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.
-->
<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>
<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,
Validators
} 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 { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { AlarmSeverityNotificationColors } from "@shared/models/notification.models";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { coerceBoolean } from "@shared/decorators/coercion";
import { Observable } from "rxjs";
@ -68,7 +67,7 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida
alarmSeverityEnum = AlarmSeverity;
alarmSeverityTranslationMap = alarmSeverityTranslations;
AlarmSeverityNotificationColors = AlarmSeverityNotificationColors;
AlarmSeverityNotificationColors = alarmSeverityColors;
createAlarmRulesFormGroup = this.fb.group({
createAlarmRules: this.fb.array<{severity: AlarmSeverity, alarmRule: AlarmRule}>([])
@ -115,13 +114,13 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida
}
createAlarmRulesControls.push(this.fb.group({
severity: [severity, Validators.required],
alarmRule: [createAlarmRule, Validators.required]
alarmRule: [{value: createAlarmRule, disabled: this.disabled}, Validators.required]
}));
});
}
const formArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray;
formArray.clear();
createAlarmRulesControls.forEach(c => formArray.push(c));
formArray.clear({emitEvent: false});
createAlarmRulesControls.forEach(c => formArray.push(c, {emitEvent: false}));
if (this.disabled) {
this.createAlarmRulesFormGroup.disable({emitEvent: false});
} else {

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

@ -32,13 +32,15 @@ import {
AlarmCommentInfo,
AlarmCommentType,
AlarmMessage,
AlarmSeverity,
alarmSeverityTranslations,
getUserDisplayName
} from '@shared/models/alarm.models';
import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models';
import { DatePipe } from '@angular/common';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { isNotEmptyStr } from '@core/utils';
import { deepClone, isNotEmptyStr } from '@core/utils';
interface AlarmCommentsDisplayData {
commentId?: string;
@ -153,8 +155,21 @@ export class AlarmCommentComponent implements OnInit {
private parseSystemComment(alarm: AlarmCommentInfo): string {
const subTypeKey = alarm.comment?.subtype;
if (subTypeKey && AlarmMessage[subTypeKey]) {
const alarmComment = deepClone(alarm.comment);
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;
}

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() {
this.updateAlarmConfigForm(this.alarmFilterConfig);
this.alarmFilterConfigForm.markAsPristine();
if (this.overlayRef) {
this.overlayRef.dispose();
} 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() {
this.updateAuditLogFilterForm(this.auditLogFilter);
this.auditLogFilterForm.markAsPristine();
this.auditLogOverlayRef.dispose();
}
update() {
this.auditLogFilterUpdated(this.auditLogFilterForm.value);
this.auditLogFilterForm.markAsPristine();
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 {
CalculatedFieldsFilterConfigComponent
} from '@home/components/calculated-fields/table-header/calculated-fields-filter-config.component';
import { CalculatedFieldComponent } from '@home/components/calculated-fields/calculated-field.component';
@NgModule({
declarations: [
@ -57,7 +58,8 @@ import {
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent,
CalculatedFieldsHeaderComponent,
CalculatedFieldsFilterConfigComponent
CalculatedFieldsFilterConfigComponent,
CalculatedFieldComponent,
],
imports: [
CommonModule,
@ -72,6 +74,7 @@ import {
exports: [
CalculatedFieldDialogComponent,
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldComponent,
]
})
export class CalculatedFieldsModule {}

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

@ -20,7 +20,7 @@ import {
EntityTableColumn,
EntityTableConfig
} 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 { Direction } from '@shared/models/page/sort-order';
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 { takeUntilDestroyed } from '@angular/core/rxjs-interop';
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 {
ArgumentEntityType,
ArgumentType,
@ -46,6 +46,7 @@ import {
CalculatedFieldsQuery,
CalculatedFieldType,
CalculatedFieldTypeTranslations,
debugCfActionEnabled,
getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights,
PropagationWithExpression,
@ -62,20 +63,26 @@ import { EntityDebugSettingsService } from '@home/components/entity/debug/entity
import { DatePipe } from '@angular/common';
import { UtilsService } from "@core/services/utils.service";
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 {
CalculatedFieldsHeaderComponent
} 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> {
readonly tenantId = getCurrentAuthUser(this.store).tenantId;
additionalDebugActionConfig = {
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;
@ -93,30 +100,35 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService,
private router: Router,
public pageMode = false,
) {
super();
if (this.pageMode) {
this.headerComponent = CalculatedFieldsHeaderComponent;
this.handleRowClick = ($event, entity) => {
this.editCalculatedField($event, entity);
this.rowPointer = true;
return true;
};
this.entityComponent = CalculatedFieldComponent;
this.entityTabsComponent = CalculatedFieldsTabsComponent;
this.rowPointer = true;
}
this.tableTitle = this.pageMode ? '' : this.translate.instant('entity.type-calculated-fields');
this.detailsPanelEnabled = false;
this.tableTitle = this.translate.instant('entity.type-calculated-fields');
this.detailsPanelEnabled = this.pageMode;
this.entityType = 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.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.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text');
this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count});
this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text');
this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id);
this.onEntityAction = action => this.onCFAction(action);
this.addActionDescriptors = [
{
name: this.translate.instant('calculated-fields.create'),
@ -163,7 +175,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
name: this.translate.instant('entity-view.events'),
icon: 'mdi:clipboard-text-clock',
isEnabled: () => true,
onAction: ($event, entity) => this.openDebugEventsDialog($event, entity),
onAction: ($event, entity) =>
this.pageMode ? this.openDebugTab($event, entity) : this.openDebugEventsDialog($event, entity),
},
{
name: '',
@ -172,14 +185,16 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
isEnabled: () => true,
iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline',
onAction: ($event, entity) => this.onOpenDebugConfig($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<CalculatedFieldsTableEntity>> {
@ -188,7 +203,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
this.calculatedFieldsService.getCalculatedFieldsByEntityId(this.entityId, pageLink);
}
onOpenDebugConfig($event: Event, calculatedField: CalculatedFieldsTableEntity): void {
private onOpenDebugConfig($event: Event, calculatedField: CalculatedFieldsTableEntity): void {
$event?.stopPropagation();
const { debugSettings = {}, id } = calculatedField;
const additionalActionConfig = {
@ -212,6 +227,22 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
}, $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 {
$event?.stopPropagation();
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 entityName = this.entityName || (value as CalculatedFieldInfo)?.entityName;
return this.dialog.open<CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedField>(CalculatedFieldDialogComponent, {
@ -234,10 +265,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
entityId,
entityName,
tenantId: this.tenantId,
ownerId: this.ownerId,
ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId},
additionalDebugActionConfig: this.additionalDebugActionConfig,
getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
isDirty,
disabledSelectType
},
enterAnimationDuration: isDirty ? 0 : null,
})
@ -245,17 +277,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
.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();
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>) => {
this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments))
this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments), openCalculatedFieldEdit)
.subscribe(expression => dialogRef.close(expression));
};
@ -270,11 +296,15 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS],
defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD,
onDebugEventSelected,
debugActionEnabledFn
debugActionDisabled: !debugCfActionEnabled(calculatedField)
}
})
.afterClosed()
.subscribe();
.subscribe(value => {
if (afterCloseCallback) {
afterCloseCallback(value)
}
});
}
private exportCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity): void {
@ -315,7 +345,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
}
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),
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)),
filter(Boolean),
@ -354,7 +384,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
).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 (
calculatedField.type === CalculatedFieldType.SCRIPT ||
calculatedField.type === CalculatedFieldType.RELATED_ENTITIES_AGGREGATION ||
@ -394,4 +424,22 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
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 { DatePipe } from '@angular/common';
import { UtilsService } from "@core/services/utils.service";
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'tb-calculated-fields-table',
@ -70,6 +70,7 @@ export class CalculatedFieldsTableComponent {
private utilsService: UtilsService,
private destroyRef: DestroyRef,
private route: ActivatedRoute,
private router: Router
) {
this.pageMode = !!this.route.snapshot.data.isPage;
effect(() => {
@ -88,6 +89,7 @@ export class CalculatedFieldsTableComponent {
this.importExportService,
this.entityDebugSettingsService,
this.utilsService,
this.router,
this.pageMode,
);
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) {
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) {
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 {
this.updateErrorText();
return this.errorText ? { argumentsFormArray: false } : null;
return this.errorText || !this.argumentsFormArray.controls.length ? { argumentsFormArray: false } : null;
}
setDisabledState(isDisabled: boolean): void {
@ -239,7 +239,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
}
writeValue(argumentsObj: Record<string, CalculatedFieldArgument>): void {
this.argumentsFormArray.clear();
this.argumentsFormArray.clear({emitEvent: false});
this.populateArgumentsFormArray(argumentsObj);
this.updateEntityNameMap(this.argumentsFormArray.value);
}
@ -249,7 +249,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
}
protected changeIsScriptMode(): void {
this.argumentsFormArray.updateValueAndValidity();
this.argumentsFormArray.updateValueAndValidity({emitEvent: !this.disable});
}
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.updateValueAndValidity();
this.updateDataSource(this.argumentsFormArray.value);
}
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>
@if (!data.entityId) {
<div class="flex flex-row gap-4 xs:flex-col" formGroupName="entityId">
<tb-entity-type-select #entityTypeSelect
appearance="outline"
class="flex-1"
showLabel
required
label="{{ 'entity.entity-type' | translate }}"
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="calculatedFieldsEntityTypeList"
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>
<tb-entity-select formControlName="entityId"
appearance="outline"
required
entityTypeLabel="{{ 'entity.entity-type' | translate }}"
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="calculatedFieldsEntityTypeList"
[defaultEntityType]="EntityType.DEVICE_PROFILE"
(entityChanged)="changeEntity($event)">
</tb-entity-select>
}
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<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.
///
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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
import {
@ -28,21 +27,18 @@ import {
CalculatedFieldTestScriptFn,
CalculatedFieldType,
calculatedFieldTypes,
CalculatedFieldTypeTranslations,
OutputStrategyType
CalculatedFieldTypeTranslations
} from '@shared/models/calculated-field.models';
import { oneSpaceInsideRegex } from '@shared/models/regex.constants';
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
import { pairwise, switchMap } from 'rxjs/operators';
import { EntityType } from '@shared/models/entity-type.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { Observable } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
import { deepTrim, isDefined } from '@core/utils';
import { EntityTypeSelectComponent } from '@shared/components/entity/entity-type-select.component';
import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component';
import { deepTrim } from '@core/utils';
import { BaseData } from '@shared/models/base-data';
import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service';
import { FormGroup } from '@angular/forms';
export interface CalculatedFieldDialogData {
value?: CalculatedField;
@ -54,6 +50,7 @@ export interface CalculatedFieldDialogData {
additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>;
getTestScriptDialogFn: CalculatedFieldTestScriptFn;
isDirty?: boolean;
disabledSelectType?: boolean;
}
@Component({
@ -64,16 +61,7 @@ export interface CalculatedFieldDialogData {
})
export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFieldDialogComponent, CalculatedField> {
fieldFormGroup = this.fb.group({
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),
});
fieldFormGroup: FormGroup;
additionalDebugActionConfig = this.data.value?.id ? {
...this.data.additionalDebugActionConfig,
@ -91,18 +79,16 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
readonly fieldTypes = calculatedFieldTypes;
readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations;
@ViewChild('entityTypeSelect') entityTypeSelect: EntityTypeSelectComponent;
@ViewChild('entityAutocompleteComponent') entityAutocompleteComponent: EntityAutocompleteComponent;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDialogData,
protected dialogRef: MatDialogRef<CalculatedFieldDialogComponent, CalculatedField>,
private calculatedFieldsService: CalculatedFieldsService,
private destroyRef: DestroyRef,
private fb: FormBuilder) {
private cfFormService: CalculatedFieldFormService) {
super(store, router, dialogRef);
this.observeType();
this.fieldFormGroup = this.cfFormService.buildForm();
this.cfFormService.setupTypeChange(this.fieldFormGroup, this.destroyRef);
this.applyDialogData();
if (this.data.isDirty) {
@ -110,7 +96,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
}
if (!this.data.entityId) {
this.fieldFormGroup.get('entityId.id').valueChanges.pipe(
this.fieldFormGroup.get('entityId').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((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 {
return deepTrim(this.fieldFormGroup.value as CalculatedField);
return deepTrim(this.fieldFormGroup.getRawValue() as CalculatedField);
}
cancel(): void {
@ -142,24 +132,17 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
});
} else {
this.fieldFormGroup.get('name').markAsTouched();
this.entityTypeSelect?.markAsTouched();
this.entityAutocompleteComponent?.markAsTouched();
}
}
onTestScript(expression?: string): Observable<string> {
const calculatedFieldId = this.data.value?.id?.id;
if (calculatedFieldId) {
return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true})
.pipe(
switchMap(event => {
const args = event?.arguments ? JSON.parse(event.arguments) : null;
return this.data.getTestScriptDialogFn(this.fromGroupValue, args, false, expression);
}),
takeUntilDestroyed(this.destroyRef)
)
}
return this.data.getTestScriptDialogFn(this.fromGroupValue, null, false, expression);
return this.cfFormService.testScript(
this.data.value?.id?.id,
this.fromGroupValue,
this.data.getTestScriptDialogFn,
this.destroyRef,
expression
);
}
changeEntity(entity: BaseData<EntityId>): void {
@ -172,28 +155,12 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
private applyDialogData(): void {
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) {
if (isDefined(configuration?.output) && !configuration?.output?.strategy) {
configuration.output.strategy = {type: OutputStrategyType.RULE_CHAIN};
}
}
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, entityId, ...value }, {emitEvent: false});
const preparedConfig = this.cfFormService.prepareConfig(configuration);
this.fieldFormGroup.patchValue({ configuration: preparedConfig, type, debugSettings, entityId, ...value }, {emitEvent: false});
setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true}));
if (!this.data.entityId) {
this.fieldFormGroup.get('configuration').disable({emitEvent: false});
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)}
}
this.entityAggregationConfiguration.patchValue(data, {emitEvent: false});
this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value);
this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value);
this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value);
this.checkProduceIntermediate();
if (this.entityAggregationConfiguration.enabled) {
this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value);
this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value);
this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value);
this.checkProduceIntermediate();
}
this.updatedOffsetHint();
setTimeout(() => {
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">
<div class="tb-form-panel stroked" *ngIf="entityType === ArgumentEntityType.RelationQuery">
<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-header">
<div class="tb-form-table-header-cell tb-actions-header"></div>
@ -221,7 +223,7 @@
</mat-form-field>
</div>
</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()">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.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 {
this.zoneGroupsFormArray.clear();
this.zoneGroupsFormArray.clear({emitEvent: false});
this.populateZonesFormArray(zonesObj);
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.updateValueAndValidity();
this.updateDataSource(this.zoneGroupsFormArray.value);
}
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 {
this.geofencingConfiguration.patchValue(config, {emitEvent: false});
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 {

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

@ -170,7 +170,7 @@
[highlightRules]="highlightRules"
[editorCompleter]="editorCompleter"
[helpPopupStyle]="{ width: '1200px' }"
helpId="calculated-field/expression_fn">
helpId="calculated-field/map_expression_fn">
<div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}
</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;
readonly AggFunctions = Object.values(AggFunction) as AggFunction[];
AggFunctions = Object.values(AggFunction) as AggFunction[];
readonly AggFunctionTranslations = AggFunctionTranslations;
readonly ScriptLanguage = ScriptLanguage;
readonly AggInputType = AggInputType;
@ -103,6 +103,10 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit {
this.validateInputKey();
this.functionArgs = ['ctx', ...this.arguments];
if (this.simpleMode) {
this.AggFunctions = this.AggFunctions.filter(aggFunc => aggFunc !== AggFunction.COUNT_UNIQUE);
}
}
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 {
this.metricsFormArray.clear();
this.metricsFormArray.clear({emitEvent: false});
this.populateZonesFormArray(metrics);
}
@ -230,7 +230,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu
};
this.metricsFormArray.push(this.fb.control(value), { emitEvent: false });
});
this.metricsFormArray.updateValueAndValidity();
this.updateDataSource(this.metricsFormArray.value);
}
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)) {
const change = changes[propName];
if (change.currentValue !== change.previousValue) {
if (propName === 'simpleMode') {
if (propName === 'simpleMode' && !this.disabled) {
this.updatedFormWithMode();
if (!change.firstChange) {
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) {
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 {
@ -253,7 +253,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
private updateTimeSeriesTtl(value: boolean) {
if (value) {
this.outputForm.get('strategy.useCustomTtl').enable({emitEvent: true});
this.outputForm.get('strategy.useCustomTtl').enable({emitEvent: false});
} else {
this.outputForm.get('strategy.useCustomTtl').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 {
value.expression = value.expression ?? calculatedFieldDefaultScript;
this.propagateConfiguration.patchValue(value, {emitEvent: false});
this.updatedFormWithScript();
if (!this.disabled) {
this.updatedFormWithScript();
}
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;
}
this.simpleConfiguration.patchValue(formValue, {emitEvent: false});
this.updatedFormWithScript();
setTimeout(() => {
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() {
this.updateCfConfigForm(this.cfFilterConfig);
this.cfFilterForm.markAsPristine();
if (this.overlayRef) {
this.overlayRef.dispose();
} 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();
cssParser.testMode = false;
this.dashboardPageClass = 'tb-dashboard-page-css-' + guid();
this.dashboardCtx.dashboardCssClass = this.dashboardPageClass;
cssParser.cssPreviewNamespace = 'tb-default .' + this.dashboardPageClass;
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>;
stateId: Observable<string>;
runChangeDetection: () => void;
dashboardCssClass?: string;
}
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() {
this.updateDeviceInfoFilterForm(this.deviceInfoFilter);
this.deviceInfoFilterForm.markAsPristine();
if (this.overlayRef) {
this.overlayRef.dispose();
} 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 { EntityDebugSettingPanelConfig } from '@home/components/entity/debug/entity-debug-settings.model';
@Injectable()
@Injectable({
providedIn: 'root'
})
export class EntityDebugSettingsService {
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 { 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 { AppState } from '@core/core.state';
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();
protected constructor(protected store: Store<AppState>) {
super(store);
this.authUser = getCurrentAuthUser(store);
protected store: Store<AppState> = inject(Store<AppState>);
protected constructor(...args: unknown[]) {
super();
this.authUser = getCurrentAuthUser(this.store);
}
ngOnInit() {

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

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

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

@ -17,10 +17,12 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component, EventEmitter,
Component,
EventEmitter,
Input,
OnDestroy,
OnInit, Output,
OnInit,
Output,
ViewChild,
ViewContainerRef
} from '@angular/core';
@ -61,6 +63,21 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
@Input()
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;
dirtyValue = false;
entityIdValue: EntityId;
@ -151,7 +168,8 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
this.store,
this.functionTestButtonLabel,
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 { MatDialog } from '@angular/material/dialog';
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 { Lwm2mSecurityType } from '@shared/models/lwm2m-security-config.models';
@ -164,10 +164,8 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed();
const addServerConfigObs = addDialogObs.pipe(
filter((isBootstrap) => isBootstrap !== null),
mergeMap((isBootstrap) => {
if (isBootstrap === null) {
return of(null);
}
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,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
panelClass: ['tb-dialog', 'tb-fullscreen-dialog', this.ctx.stateController.dashboardCtrl.dashboardCtx.dashboardCssClass, this.ctx.widgetCssClass],
data: {
alarmId: alarm.id.id,
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(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
this.colorRangeListFormGroup.get('advancedMode').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => setTimeout(() => {this.popover?.updatePosition();}, 0));
}
ngOnDestroy() {
@ -179,7 +176,6 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
public removeAdvancedRange(index: number) {
(this.colorRangeListFormGroup.get('rangeAdvanced') as UntypedFormArray).removeAt(index);
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
get advancedRangeFormArray(): UntypedFormArray {
@ -193,7 +189,6 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
removeRange(index: number) {
this.rangeListFormArray.removeAt(index);
this.colorRangeListFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
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 advancedRangeColorControl = this.fb.control(advancedRange, [advancedRangeValidator]);
advancedRangeColorsArray.push(advancedRangeColorControl);
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
addRange() {
@ -228,7 +222,6 @@ export class ColorRangeListComponent implements OnInit, ControlValueAccessor, On
};
this.rangeListFormArray.push(this.colorRangeControl(newRange));
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>
<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"
[functionArgs]="['value']"
[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 { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { Datasource } from '@shared/models/widget.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'tb-color-settings-panel',
@ -107,12 +106,6 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit
colorFunction: [this.colorSettings?.colorFunction, []]
}
);
this.colorSettingsFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateValidators();
setTimeout(() => {this.popover?.updatePosition();}, 0);
});
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(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
this.gradientFormGroup.get('advancedMode').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => setTimeout(() => {this.popover?.updatePosition();}, 0));
}
ngOnDestroy() {
@ -236,7 +233,6 @@ export class GradientComponent implements OnInit, ControlValueAccessor, OnDestro
this.gradientListFormArray.removeAt(index);
}
this.gradientFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
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.gradientFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
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;
const cssString = this.widget.widget.config.widgetCss;
if (isNotEmptyStr(cssString)) {
this.cssClass =
this.utils.applyCssToElement(this.renderer, this.gridsterItem.el, 'tb-widget-css', cssString);
this.cssClass = this.utils.applyCssToElement(this.renderer, this.gridsterItem.el, 'tb-widget-css', cssString, true);
this.widget.widgetContext.widgetCssClass = this.cssClass;
}
$(this.gridsterItem.el).on('mousedown', (e) => this.onMouseDown(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"
[entityId]="data.entityId"
[functionTestButtonLabel]="data.functionTestButtonLabel ?? ''"
[disableDebugEventAction]="data.debugActionDisabled"
(debugEventSelected)="this.onDebugEventSelected($event)"
/>
</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 { DebugEventType, EventType } from '@shared/models/event.models';
import { EntityId } from '@shared/models/id/entity-id';
import { BaseData, HasId } from '@shared/models/base-data';
export interface EventsDialogData {
title: string;
@ -34,7 +33,7 @@ export interface EventsDialogData {
disabledEventTypes?: Array<EventType | DebugEventType>;
functionTestButtonLabel?: string;
onDebugEventSelected?: (event: any, dialogRef: MatDialogRef<EventsDialogComponent, string>) => void;
debugActionEnabledFn?: (event: BaseData<HasId>) => boolean;
debugActionDisabled?: boolean;
}
@Component({
@ -54,9 +53,6 @@ export class EventsDialogComponent extends DialogComponent<EventsDialogComponent
}
ngAfterViewInit() {
if (this.data.debugActionEnabledFn && this.eventsTable.entitiesTable.cellActionDescriptors?.length > 0) {
this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = this.data.debugActionEnabledFn;
}
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 { Timewindow } from '@shared/models/time/time.models';
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 { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { EntityAction } from '@home/models/entity/entity-component.models';
import {
CellActionDescriptor, EntityActionTableColumn, EntityColumn, EntityTableColumn,
CellActionDescriptor,
EntityActionTableColumn,
EntityColumn,
EntityTableColumn,
EntityTableConfig,
GroupActionDescriptor,
HeaderActionDescriptor
} from '@home/models/entity/entities-table-config.models';
import { ActivatedRoute } from '@angular/router';
import type { EntityDetailsPanelComponent } from '@home/components/entity/entity-details-panel.component';
export type EntitiesTableAction = 'add';
@ -64,6 +68,7 @@ export interface IEntitiesTableComponent {
paginator: MatPaginator;
sort: MatSort;
route: ActivatedRoute;
entityDetailsPanel: EntityDetailsPanelComponent;
viewContainerRef: ViewContainerRef;
addEnabled(): boolean;

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

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

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

@ -14,14 +14,64 @@
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DestroyRef, inject, NgModule } from '@angular/core';
import { ActivatedRouteSnapshot, ResolveFn, Router, RouterModule, RouterStateSnapshot, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum';
import { AlarmTableComponent } from '@home/components/alarm/alarm-table.component';
import { AlarmsMode } from '@shared/models/alarm.models';
import { MenuId } from '@core/services/menu.models';
import { RouterTabsComponent } from "@home/components/router-tabs.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 = [
{
@ -57,15 +107,38 @@ const routes: Routes = [
},
{
path: 'alarm-rules',
component: AlarmRulesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'alarm-rule.alarm-rules',
breadcrumb: {
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 { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { AlarmRoutingModule } from '@home/pages/alarm/alarm-routing.module';
import { AlarmRulesTabsComponent } from '@home/pages/alarm/alarm-rules-tabs.component';
@NgModule({
declarations: [],
declarations: [
AlarmRulesTabsComponent
],
imports: [
CommonModule,
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 { HomeComponentsModule } from '@modules/home/components/home-components.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({
declarations: [],
declarations: [
CalculatedFieldsTabsComponent
],
imports: [
CommonModule,
SharedModule,
HomeComponentsModule,
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.
///
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DestroyRef, inject, NgModule } from '@angular/core';
import { ActivatedRouteSnapshot, ResolveFn, Router, RouterModule, RouterStateSnapshot, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum';
import { MenuId } from '@core/services/menu.models';
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 = [
{
path: 'calculatedFields',
component: CalculatedFieldsTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'entity.type-calculated-fields',
breadcrumb: {
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 { AssetModule } from '@modules/home/pages/asset/asset.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 { WidgetLibraryModule } from '@modules/home/pages/widget/widget-library.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,
EntityViewModule,
CustomerModule,
CalculatedFieldsModule,
CalculatedFieldPageModule,
RuleChainModule,
WidgetLibraryModule,
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-header>
<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>
<span style="height: 4px;" *ngIf="!isLoading"></span>
<span style="height: 4px;" *ngIf="!isLoading && !(isLoading$ | async)"></span>
<mat-card-content class="!p-6">
<form [formGroup]="createPassword" (ngSubmit)="onCreatePassword()">
<mat-form-field class="mat-block tb-autofilled" appearance="outline">
@ -53,10 +53,10 @@
</mat-error>
</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">
<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 }}
</button>
<button mat-button type="button" [disabled]="isLoading" routerLink="/login">
<button mat-button type="button" [disabled]="isLoading || (isLoading$ | async)" routerLink="/login">
{{ 'action.cancel' | translate }}
</button>
</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 { UserPasswordPolicy } from '@shared/models/settings.models';
import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/models/password.models';
import { finalize } from 'rxjs/operators';
import { PageComponent } from '@shared/components/page.component';
@Component({
selector: 'tb-create-password',
templateUrl: './create-password.component.html',
styleUrls: ['./password.component.scss']
})
export class CreatePasswordComponent {
export class CreatePasswordComponent extends PageComponent {
passwordPolicy: UserPasswordPolicy;
createPassword: FormGroup;
@ -38,7 +40,7 @@ export class CreatePasswordComponent {
constructor(private route: ActivatedRoute,
private authService: AuthService,
private fb: FormBuilder) {
super();
this.activateToken = this.route.snapshot.queryParams['activateToken'] || '';
this.passwordPolicy = this.route.snapshot.data['passwordPolicy'];
@ -60,11 +62,10 @@ export class CreatePasswordComponent {
if (this.createPassword.invalid) {
this.createPassword.markAllAsTouched();
} else {
this.isLoading = true
this.authService.activate(this.activateToken, this.createPassword.get('newPassword').value, true)
.subscribe({
error: () => {this.isLoading = false;}
});
this.isLoading = true;
this.authService.activate(this.activateToken, this.createPassword.get('newPassword').value, true).pipe(
finalize(() => {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;">
<tb-logo link="https://thingsboard.io" target="_blank" class="login-logo"></tb-logo>
</div>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading">
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading || (isLoading$ | async)">
</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>
<div class="flex flex-col">
@if(oauth2Clients?.length) {
@ -70,7 +70,7 @@
<tb-toggle-password matSuffix></tb-toggle-password>
<mat-icon matPrefix>lock</mat-icon>
</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>
<div class="forgot-password flex items-center justify-end gap-5">
<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 { OAuth2ClientLoginInfo } from '@shared/models/oauth2.models';
import { validateEmail } from '@app/core/utils';
import { PageComponent } from '@shared/components/page.component';
import { finalize } from 'rxjs/operators';
@Component({
selector: 'tb-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
export class LoginComponent extends PageComponent implements OnInit {
passwordViolation = false;
isLoading = false;
@ -42,6 +44,7 @@ export class LoginComponent implements OnInit {
constructor(private authService: AuthService,
public fb: UntypedFormBuilder,
private router: Router) {
super();
}
ngOnInit() {
@ -51,10 +54,10 @@ export class LoginComponent implements OnInit {
login(): void {
if (this.loginFormGroup.valid) {
this.isLoading = true;
this.authService.login(this.loginFormGroup.value).subscribe({
next: () => {},
this.authService.login(this.loginFormGroup.value).pipe(
finalize(() => {this.isLoading = false;})
).subscribe({
error: (error: HttpErrorResponse) => {
this.isLoading = false;
if (error && error.error && error.error.errorCode) {
if (error.error.errorCode === Constants.serverErrorCode.credentialsExpired) {
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 { UserPasswordPolicy } from '@shared/models/settings.models';
import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/models/password.models';
import { finalize } from 'rxjs/operators';
@Component({
selector: 'tb-reset-password',
@ -64,10 +65,9 @@ export class ResetPasswordComponent {
this.resetPassword.markAllAsTouched();
} else {
this.isLoading = true;
this.authService.resetPassword(this.resetToken, this.resetPassword.get('newPassword').value).subscribe({
next: () => this.router.navigateByUrl('login'),
error: () => {this.isLoading = false;}
});
this.authService.resetPassword(this.resetToken, this.resetPassword.get('newPassword').value).pipe(
finalize(() => {this.isLoading = false})
).subscribe(() => this.router.navigateByUrl('login'));
}
}
}

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

Loading…
Cancel
Save