Merge branch 'feature/calculated-fields' of github.com:thingsboard/thingsboard into calculated-fields
This commit is contained in:
commit
83f55ebf9d
@ -314,6 +314,7 @@ public class ActorSystemContext {
|
|||||||
@Getter
|
@Getter
|
||||||
private TbEntityViewService tbEntityViewService;
|
private TbEntityViewService tbEntityViewService;
|
||||||
|
|
||||||
|
@Lazy
|
||||||
@Autowired
|
@Autowired
|
||||||
@Getter
|
@Getter
|
||||||
private TelemetrySubscriptionService tsSubService;
|
private TelemetrySubscriptionService tsSubService;
|
||||||
|
|||||||
@ -18,6 +18,7 @@ package org.thingsboard.server.actors.calculatedField;
|
|||||||
import org.thingsboard.server.actors.ActorSystemContext;
|
import org.thingsboard.server.actors.ActorSystemContext;
|
||||||
import org.thingsboard.server.actors.TbActor;
|
import org.thingsboard.server.actors.TbActor;
|
||||||
import org.thingsboard.server.actors.TbActorId;
|
import org.thingsboard.server.actors.TbActorId;
|
||||||
|
import org.thingsboard.server.actors.TbCalculatedFieldEntityActorId;
|
||||||
import org.thingsboard.server.actors.TbEntityActorId;
|
import org.thingsboard.server.actors.TbEntityActorId;
|
||||||
import org.thingsboard.server.actors.device.DeviceActor;
|
import org.thingsboard.server.actors.device.DeviceActor;
|
||||||
import org.thingsboard.server.actors.service.ContextBasedCreator;
|
import org.thingsboard.server.actors.service.ContextBasedCreator;
|
||||||
@ -38,7 +39,7 @@ public class CalculatedFieldEntityActorCreator extends ContextBasedCreator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TbActorId createActorId() {
|
public TbActorId createActorId() {
|
||||||
return new TbEntityActorId(entityId);
|
return new TbCalculatedFieldEntityActorId(entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import org.thingsboard.server.actors.TbCalculatedFieldEntityActorId;
|
|||||||
import org.thingsboard.server.actors.service.DefaultActorService;
|
import org.thingsboard.server.actors.service.DefaultActorService;
|
||||||
import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
|
import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
|
||||||
import org.thingsboard.server.common.data.EntityType;
|
import org.thingsboard.server.common.data.EntityType;
|
||||||
|
import org.thingsboard.server.common.data.cf.CalculatedField;
|
||||||
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
|
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
|
||||||
import org.thingsboard.server.common.data.id.AssetId;
|
import org.thingsboard.server.common.data.id.AssetId;
|
||||||
import org.thingsboard.server.common.data.id.CalculatedFieldId;
|
import org.thingsboard.server.common.data.id.CalculatedFieldId;
|
||||||
@ -50,7 +51,6 @@ import java.util.Collections;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
@ -175,7 +175,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
|
|||||||
private void onEntityCreated(ComponentLifecycleMsg msg, TbCallback callback) {
|
private void onEntityCreated(ComponentLifecycleMsg msg, TbCallback callback) {
|
||||||
EntityId entityId = msg.getEntityId();
|
EntityId entityId = msg.getEntityId();
|
||||||
EntityId profileId = getProfileId(tenantId, entityId);
|
EntityId profileId = getProfileId(tenantId, entityId);
|
||||||
cfEntityCache.add(tenantId, entityId, profileId);
|
cfEntityCache.add(tenantId, profileId, entityId);
|
||||||
var entityIdFields = getCalculatedFieldsByEntityId(entityId);
|
var entityIdFields = getCalculatedFieldsByEntityId(entityId);
|
||||||
var profileIdFields = getCalculatedFieldsByEntityId(profileId);
|
var profileIdFields = getCalculatedFieldsByEntityId(profileId);
|
||||||
var fieldsCount = entityIdFields.size() + profileIdFields.size();
|
var fieldsCount = entityIdFields.size() + profileIdFields.size();
|
||||||
@ -233,6 +233,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
|
|||||||
// We use copy on write lists to safely pass the reference to another actor for the iteration.
|
// We use copy on write lists to safely pass the reference to another actor for the iteration.
|
||||||
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
|
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
|
||||||
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
|
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
|
||||||
|
addLinks(cf);
|
||||||
initCf(cfCtx, callback, false);
|
initCf(cfCtx, callback, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -251,7 +252,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
|
|||||||
} else {
|
} else {
|
||||||
var newCfCtx = new CalculatedFieldCtx(newCf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService());
|
var newCfCtx = new CalculatedFieldCtx(newCf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService());
|
||||||
calculatedFields.put(newCf.getId(), newCfCtx);
|
calculatedFields.put(newCf.getId(), newCfCtx);
|
||||||
List<CalculatedFieldCtx> oldCfList = entityIdCalculatedFields.get(newCf.getId());
|
List<CalculatedFieldCtx> oldCfList = entityIdCalculatedFields.get(newCf.getEntityId());
|
||||||
List<CalculatedFieldCtx> newCfList = new ArrayList<>(oldCfList.size());
|
List<CalculatedFieldCtx> newCfList = new ArrayList<>(oldCfList.size());
|
||||||
boolean found = false;
|
boolean found = false;
|
||||||
for (CalculatedFieldCtx oldCtx : oldCfList) {
|
for (CalculatedFieldCtx oldCtx : oldCfList) {
|
||||||
@ -265,10 +266,15 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
|
|||||||
if (!found) {
|
if (!found) {
|
||||||
newCfList.add(newCfCtx);
|
newCfList.add(newCfCtx);
|
||||||
}
|
}
|
||||||
entityIdCalculatedFields.put(newCf.getId(), newCfList);
|
entityIdCalculatedFields.put(newCf.getEntityId(), newCfList);
|
||||||
|
|
||||||
|
deleteLinks(oldCfCtx);
|
||||||
|
addLinks(newCf);
|
||||||
|
|
||||||
// We use copy on write lists to safely pass the reference to another actor for the iteration.
|
// We use copy on write lists to safely pass the reference to another actor for the iteration.
|
||||||
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
|
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
|
||||||
if (newCfCtx.hasSignificantChanges(oldCfCtx)) {
|
var stateChanges = newCfCtx.hasStateChanges(oldCfCtx);
|
||||||
|
if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) {
|
||||||
try {
|
try {
|
||||||
newCfCtx.init();
|
newCfCtx.init();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -276,11 +282,12 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
|
|||||||
systemContext.persistCalculatedFieldDebugEvent(newCf.getTenantId(), newCf.getId(), newCf.getEntityId(), null, null, null, null, e);
|
systemContext.persistCalculatedFieldDebugEvent(newCf.getTenantId(), newCf.getId(), newCf.getEntityId(), null, null, null, null, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initCf(newCfCtx, callback, true);
|
initCf(newCfCtx, callback, stateChanges);
|
||||||
|
} else {
|
||||||
|
callback.onSuccess();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onCfDeleted(ComponentLifecycleMsg msg, TbCallback callback) {
|
private void onCfDeleted(ComponentLifecycleMsg msg, TbCallback callback) {
|
||||||
@ -290,6 +297,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
|
|||||||
log.warn("[{}] CF was already deleted [{}]", tenantId, cfId);
|
log.warn("[{}] CF was already deleted [{}]", tenantId, cfId);
|
||||||
callback.onSuccess();
|
callback.onSuccess();
|
||||||
} else {
|
} else {
|
||||||
|
deleteLinks(cfCtx);
|
||||||
|
|
||||||
EntityId entityId = cfCtx.getEntityId();
|
EntityId entityId = cfCtx.getEntityId();
|
||||||
EntityType entityType = cfCtx.getEntityId().getEntityType();
|
EntityType entityType = cfCtx.getEntityId().getEntityType();
|
||||||
if (isProfileEntity(entityType)) {
|
if (isProfileEntity(entityType)) {
|
||||||
@ -440,4 +449,16 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
|
|||||||
() -> true);
|
() -> true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addLinks(CalculatedField newCf) {
|
||||||
|
var newLinks = newCf.getConfiguration().buildCalculatedFieldLinks(tenantId, newCf.getEntityId(), newCf.getId());
|
||||||
|
newLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new ArrayList<>()).add(link));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteLinks(CalculatedFieldCtx cfCtx) {
|
||||||
|
var oldCf = cfCtx.getCalculatedField();
|
||||||
|
var oldLinks = oldCf.getConfiguration().buildCalculatedFieldLinks(tenantId, oldCf.getEntityId(), oldCf.getId());
|
||||||
|
oldLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new ArrayList<>()).remove(link));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -70,20 +70,25 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
|
|||||||
|
|
||||||
@AfterStartUp(order = AfterStartUp.CF_READ_CF_SERVICE)
|
@AfterStartUp(order = AfterStartUp.CF_READ_CF_SERVICE)
|
||||||
public void init() {
|
public void init() {
|
||||||
|
//TODO: move to separate place to avoid circular references with the ActorSystemContext (@Lazy for tsSubService)
|
||||||
PageDataIterable<CalculatedField> cfs = new PageDataIterable<>(calculatedFieldService::findAllCalculatedFields, initFetchPackSize);
|
PageDataIterable<CalculatedField> cfs = new PageDataIterable<>(calculatedFieldService::findAllCalculatedFields, initFetchPackSize);
|
||||||
cfs.forEach(cf -> calculatedFields.putIfAbsent(cf.getId(), cf));
|
cfs.forEach(cf -> {
|
||||||
calculatedFields.values().forEach(cf ->
|
calculatedFields.putIfAbsent(cf.getId(), cf);
|
||||||
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cf)
|
actorSystemContext.tell(new CalculatedFieldInitMsg(cf.getTenantId(), cf));
|
||||||
);
|
});
|
||||||
cfs.forEach(cf -> actorSystemContext.tell(new CalculatedFieldInitMsg(cf.getTenantId(), cf)));
|
calculatedFields.values().forEach(cf -> {
|
||||||
|
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cf);
|
||||||
|
});
|
||||||
PageDataIterable<CalculatedFieldLink> cfls = new PageDataIterable<>(calculatedFieldService::findAllCalculatedFieldLinks, initFetchPackSize);
|
PageDataIterable<CalculatedFieldLink> cfls = new PageDataIterable<>(calculatedFieldService::findAllCalculatedFieldLinks, initFetchPackSize);
|
||||||
cfls.forEach(link -> calculatedFieldLinks.computeIfAbsent(link.getCalculatedFieldId(), id -> new CopyOnWriteArrayList<>()).add(link));
|
cfls.forEach(link -> {
|
||||||
|
calculatedFieldLinks.computeIfAbsent(link.getCalculatedFieldId(), id -> new CopyOnWriteArrayList<>()).add(link);
|
||||||
|
actorSystemContext.tell(new CalculatedFieldLinkInitMsg(link.getTenantId(), link));
|
||||||
|
});
|
||||||
calculatedFieldLinks.values().stream()
|
calculatedFieldLinks.values().stream()
|
||||||
.flatMap(List::stream)
|
.flatMap(List::stream)
|
||||||
.forEach(link ->
|
.forEach(link ->
|
||||||
entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link)
|
entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link)
|
||||||
);
|
);
|
||||||
cfls.forEach(link -> actorSystemContext.tell(new CalculatedFieldLinkInitMsg(link.getTenantId(), link)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -136,7 +136,6 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private final CalculatedFieldService calculatedFieldService;
|
|
||||||
private final TbAssetProfileCache assetProfileCache;
|
private final TbAssetProfileCache assetProfileCache;
|
||||||
private final TbDeviceProfileCache deviceProfileCache;
|
private final TbDeviceProfileCache deviceProfileCache;
|
||||||
private final CalculatedFieldCache calculatedFieldCache;
|
private final CalculatedFieldCache calculatedFieldCache;
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.service.cf.cache;
|
package org.thingsboard.server.service.cf.cache;
|
||||||
|
|
||||||
|
import org.thingsboard.server.common.data.EntityType;
|
||||||
import org.thingsboard.server.common.data.id.EntityId;
|
import org.thingsboard.server.common.data.id.EntityId;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -88,6 +89,9 @@ public class TenantEntityProfileCache {
|
|||||||
public void add(EntityId profileId, EntityId entityId, Integer partition, boolean mine) {
|
public void add(EntityId profileId, EntityId entityId, Integer partition, boolean mine) {
|
||||||
lock.writeLock().lock();
|
lock.writeLock().lock();
|
||||||
try {
|
try {
|
||||||
|
if(EntityType.DEVICE.equals(profileId.getEntityType())){
|
||||||
|
throw new RuntimeException("WTF?");
|
||||||
|
}
|
||||||
if (mine) {
|
if (mine) {
|
||||||
myEntities.computeIfAbsent(profileId, k -> new HashSet<>()).add(entityId);
|
myEntities.computeIfAbsent(profileId, k -> new HashSet<>()).add(entityId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -212,12 +212,16 @@ public class CalculatedFieldCtx {
|
|||||||
return new CalculatedFieldEntityCtxId(tenantId, cfId, entityId);
|
return new CalculatedFieldEntityCtxId(tenantId, cfId, entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasSignificantChanges(CalculatedFieldCtx other) {
|
public boolean hasOtherSignificantChanges(CalculatedFieldCtx other) {
|
||||||
boolean entityIdChanged = !entityId.equals(other.entityId);
|
boolean expressionChanged = !expression.equals(other.expression);
|
||||||
|
boolean outputChanged = !output.equals(other.output);
|
||||||
|
return expressionChanged || outputChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasStateChanges(CalculatedFieldCtx other) {
|
||||||
boolean typeChanged = !cfType.equals(other.cfType);
|
boolean typeChanged = !cfType.equals(other.cfType);
|
||||||
boolean argumentsChanged = !arguments.equals(other.arguments);
|
boolean argumentsChanged = !arguments.equals(other.arguments);
|
||||||
boolean expressionChanged = !expression.equals(other.expression);
|
return typeChanged || argumentsChanged;
|
||||||
return entityIdChanged || typeChanged || argumentsChanged || expressionChanged;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -178,11 +178,11 @@ public class EntityStateSourcingListener {
|
|||||||
}
|
}
|
||||||
case TENANT_PROFILE -> {
|
case TENANT_PROFILE -> {
|
||||||
TenantProfile tenantProfile = (TenantProfile) event.getEntity();
|
TenantProfile tenantProfile = (TenantProfile) event.getEntity();
|
||||||
tbClusterService.onTenantProfileDelete(tenantProfile, null);
|
tbClusterService.onTenantProfileDelete(tenantProfile, TbQueueCallback.EMPTY);
|
||||||
}
|
}
|
||||||
case DEVICE -> {
|
case DEVICE -> {
|
||||||
Device device = (Device) event.getEntity();
|
Device device = (Device) event.getEntity();
|
||||||
tbClusterService.onDeviceDeleted(tenantId, device, null);
|
tbClusterService.onDeviceDeleted(tenantId, device, TbQueueCallback.EMPTY);
|
||||||
}
|
}
|
||||||
case DEVICE_PROFILE -> {
|
case DEVICE_PROFILE -> {
|
||||||
DeviceProfile deviceProfile = (DeviceProfile) event.getEntity();
|
DeviceProfile deviceProfile = (DeviceProfile) event.getEntity();
|
||||||
@ -190,11 +190,11 @@ public class EntityStateSourcingListener {
|
|||||||
}
|
}
|
||||||
case TB_RESOURCE -> {
|
case TB_RESOURCE -> {
|
||||||
TbResourceInfo tbResource = (TbResourceInfo) event.getEntity();
|
TbResourceInfo tbResource = (TbResourceInfo) event.getEntity();
|
||||||
tbClusterService.onResourceDeleted(tbResource, null);
|
tbClusterService.onResourceDeleted(tbResource, TbQueueCallback.EMPTY);
|
||||||
}
|
}
|
||||||
case CALCULATED_FIELD -> {
|
case CALCULATED_FIELD -> {
|
||||||
CalculatedField calculatedField = (CalculatedField) event.getEntity();
|
CalculatedField calculatedField = (CalculatedField) event.getEntity();
|
||||||
tbClusterService.onCalculatedFieldDeleted(calculatedField, null);
|
tbClusterService.onCalculatedFieldDeleted(calculatedField, TbQueueCallback.EMPTY);
|
||||||
}
|
}
|
||||||
default -> {
|
default -> {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -727,14 +727,19 @@ public class DefaultTbClusterService implements TbClusterService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCalculatedFieldUpdated(CalculatedField calculatedField, CalculatedField oldCalculatedField, TbQueueCallback callback) {
|
public void onCalculatedFieldUpdated(CalculatedField calculatedField, CalculatedField oldCalculatedField, TbQueueCallback callback) {
|
||||||
var msg = new ComponentLifecycleMsg(calculatedField.getTenantId(), calculatedField.getId(), oldCalculatedField == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
|
var msg = toProto(new ComponentLifecycleMsg(calculatedField.getTenantId(), calculatedField.getId(), oldCalculatedField == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED));
|
||||||
broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), callback);
|
onCalculatedFieldLifecycleMsg(msg, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCalculatedFieldDeleted(CalculatedField calculatedField, TbQueueCallback callback) {
|
public void onCalculatedFieldDeleted(CalculatedField calculatedField, TbQueueCallback callback) {
|
||||||
var msg = new ComponentLifecycleMsg(calculatedField.getTenantId(), calculatedField.getId(), ComponentLifecycleEvent.DELETED);
|
var msg = toProto(new ComponentLifecycleMsg(calculatedField.getTenantId(), calculatedField.getId(), ComponentLifecycleEvent.DELETED));
|
||||||
broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), callback);
|
onCalculatedFieldLifecycleMsg(msg, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCalculatedFieldLifecycleMsg(ComponentLifecycleMsgProto msg, TbQueueCallback callback) {
|
||||||
|
broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(msg).build(), callback);
|
||||||
|
broadcastToCore(ToCoreNotificationMsg.newBuilder().setComponentLifecycle(msg).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -159,7 +159,8 @@ public final class TbActorMailbox implements TbActorCtx {
|
|||||||
stopReason = TbActorStopReason.INIT_FAILED;
|
stopReason = TbActorStopReason.INIT_FAILED;
|
||||||
destroy(updateException.getCause());
|
destroy(updateException.getCause());
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
log.debug("[{}] Failed to process message: {}", selfId, msg, t);
|
//TODO: revert;
|
||||||
|
log.error("[{}] Failed to process message: {}", selfId, msg, t);
|
||||||
ProcessFailureStrategy strategy = actor.onProcessFailure(msg, t);
|
ProcessFailureStrategy strategy = actor.onProcessFailure(msg, t);
|
||||||
if (strategy.isStop()) {
|
if (strategy.isStop()) {
|
||||||
system.stop(selfId);
|
system.stop(selfId);
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
|
|||||||
import { catchError, filter, switchMap } from 'rxjs/operators';
|
import { catchError, filter, switchMap } from 'rxjs/operators';
|
||||||
import { CalculatedField, CalculatedFieldDialogData } from '@shared/models/calculated-field.models';
|
import { CalculatedField, CalculatedFieldDialogData } from '@shared/models/calculated-field.models';
|
||||||
import { CalculatedFieldDialogComponent } from './components/public-api';
|
import { CalculatedFieldDialogComponent } from './components/public-api';
|
||||||
|
import { ImportExportService } from '@shared/import-export/import-export.service';
|
||||||
|
|
||||||
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField, PageLink> {
|
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField, PageLink> {
|
||||||
|
|
||||||
@ -55,7 +56,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
|
|||||||
private popoverService: TbPopoverService,
|
private popoverService: TbPopoverService,
|
||||||
private destroyRef: DestroyRef,
|
private destroyRef: DestroyRef,
|
||||||
private renderer: Renderer2,
|
private renderer: Renderer2,
|
||||||
public entityName: string
|
public entityName: string,
|
||||||
|
private importExportService: ImportExportService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.tableTitle = this.translate.instant('entity.type-calculated-fields');
|
this.tableTitle = this.translate.instant('entity.type-calculated-fields');
|
||||||
@ -71,6 +73,20 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
|
|||||||
this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count});
|
this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count});
|
||||||
this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text');
|
this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text');
|
||||||
this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id);
|
this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id);
|
||||||
|
this.addActionDescriptors = [
|
||||||
|
{
|
||||||
|
name: this.translate.instant('calculated-fields.create'),
|
||||||
|
icon: 'insert_drive_file',
|
||||||
|
isEnabled: () => true,
|
||||||
|
onAction: ($event) => this.getTable().addEntity($event)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: this.translate.instant('calculated-fields.import'),
|
||||||
|
icon: 'file_upload',
|
||||||
|
isEnabled: () => true,
|
||||||
|
onAction: () => this.importCalculatedField()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
this.defaultSortOrder = {property: 'name', direction: Direction.DESC};
|
this.defaultSortOrder = {property: 'name', direction: Direction.DESC};
|
||||||
|
|
||||||
@ -82,6 +98,12 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
|
|||||||
this.columns.push(expressionColumn);
|
this.columns.push(expressionColumn);
|
||||||
|
|
||||||
this.cellActionDescriptors.push(
|
this.cellActionDescriptors.push(
|
||||||
|
{
|
||||||
|
name: this.translate.instant('action.export'),
|
||||||
|
icon: 'file_download',
|
||||||
|
isEnabled: () => true,
|
||||||
|
onAction: (event$, entity) => this.exportCalculatedField(event$, entity),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '',
|
name: '',
|
||||||
nameFunction: entity => this.getDebugConfigLabel(entity?.debugSettings),
|
nameFunction: entity => this.getDebugConfigLabel(entity?.debugSettings),
|
||||||
@ -166,6 +188,19 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
|
|||||||
.afterClosed();
|
.afterClosed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private exportCalculatedField($event: Event, calculatedField: CalculatedField): void {
|
||||||
|
if ($event) {
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
this.importExportService.exportCalculatedField(calculatedField.id.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private importCalculatedField(): void {
|
||||||
|
this.importExportService.importCalculatedField(this.entityId)
|
||||||
|
.pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef))
|
||||||
|
.subscribe(() => this.updateData());
|
||||||
|
}
|
||||||
|
|
||||||
private getDebugConfigLabel(debugSettings: EntityDebugSettings): string {
|
private getDebugConfigLabel(debugSettings: EntityDebugSettings): string {
|
||||||
const isDebugActive = this.isDebugActive(debugSettings?.allEnabledUntil);
|
const isDebugActive = this.isDebugActive(debugSettings?.allEnabledUntil);
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/
|
|||||||
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
|
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
|
||||||
import { TbPopoverService } from '@shared/components/popover.service';
|
import { TbPopoverService } from '@shared/components/popover.service';
|
||||||
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
|
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
|
||||||
|
import { ImportExportService } from '@shared/import-export/import-export.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-calculated-fields-table',
|
selector: 'tb-calculated-fields-table',
|
||||||
@ -59,6 +60,7 @@ export class CalculatedFieldsTableComponent {
|
|||||||
private popoverService: TbPopoverService,
|
private popoverService: TbPopoverService,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
private renderer: Renderer2,
|
private renderer: Renderer2,
|
||||||
|
private importExportService: ImportExportService,
|
||||||
private destroyRef: DestroyRef) {
|
private destroyRef: DestroyRef) {
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
@ -73,7 +75,8 @@ export class CalculatedFieldsTableComponent {
|
|||||||
this.popoverService,
|
this.popoverService,
|
||||||
this.destroyRef,
|
this.destroyRef,
|
||||||
this.renderer,
|
this.renderer,
|
||||||
this.entityName()
|
this.entityName(),
|
||||||
|
this.importExportService
|
||||||
);
|
);
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,7 +71,7 @@ export class ImportDialogComponent extends DialogComponent<ImportDialogComponent
|
|||||||
this.importFormGroup = this.fb.group({
|
this.importFormGroup = this.fb.group({
|
||||||
importType: ['file'],
|
importType: ['file'],
|
||||||
fileContent: [null, [Validators.required]],
|
fileContent: [null, [Validators.required]],
|
||||||
jsonContent: [null, [Validators.required]]
|
jsonContent: [{ value: null, disabled: true }, [Validators.required]]
|
||||||
});
|
});
|
||||||
this.importFormGroup.get('importType').valueChanges.pipe(
|
this.importFormGroup.get('importType').valueChanges.pipe(
|
||||||
takeUntilDestroyed(this.destroyRef)
|
takeUntilDestroyed(this.destroyRef)
|
||||||
|
|||||||
@ -88,6 +88,8 @@ import {
|
|||||||
ExportResourceDialogDialogResult
|
ExportResourceDialogDialogResult
|
||||||
} from '@shared/import-export/export-resource-dialog.component';
|
} from '@shared/import-export/export-resource-dialog.component';
|
||||||
import { FormProperty, propertyValid } from '@shared/models/dynamic-form.models';
|
import { FormProperty, propertyValid } from '@shared/models/dynamic-form.models';
|
||||||
|
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
|
||||||
|
import { CalculatedField } from '@shared/models/calculated-field.models';
|
||||||
|
|
||||||
export type editMissingAliasesFunction = (widgets: Array<Widget>, isSingleWidget: boolean,
|
export type editMissingAliasesFunction = (widgets: Array<Widget>, isSingleWidget: boolean,
|
||||||
customTitle: string, missingEntityAliases: EntityAliases) => Observable<EntityAliases>;
|
customTitle: string, missingEntityAliases: EntityAliases) => Observable<EntityAliases>;
|
||||||
@ -116,6 +118,7 @@ export class ImportExportService {
|
|||||||
private imageService: ImageService,
|
private imageService: ImageService,
|
||||||
private utils: UtilsService,
|
private utils: UtilsService,
|
||||||
private itembuffer: ItemBufferService,
|
private itembuffer: ItemBufferService,
|
||||||
|
private calculatedFieldsService: CalculatedFieldsService,
|
||||||
private dialog: MatDialog) {
|
private dialog: MatDialog) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -171,6 +174,35 @@ export class ImportExportService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public exportCalculatedField(calculatedFieldId: string): void {
|
||||||
|
this.calculatedFieldsService.getCalculatedFieldById(calculatedFieldId).subscribe({
|
||||||
|
next: (calculatedField) => {
|
||||||
|
let name = calculatedField.name;
|
||||||
|
name = name.toLowerCase().replace(/\W/g, '_');
|
||||||
|
this.exportToPc(this.prepareCalculatedFieldExport(calculatedField), name);
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.handleExportError(e, 'calculated-fields.export-failed-error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public importCalculatedField(entityId: EntityId): Observable<CalculatedField> {
|
||||||
|
return this.openImportDialog('calculated-fields.import', 'calculated-fields.file').pipe(
|
||||||
|
mergeMap((calculatedField: CalculatedField) => {
|
||||||
|
if (!this.validateImportedCalculatedField({ entityId, ...calculatedField })) {
|
||||||
|
this.store.dispatch(new ActionNotificationShow(
|
||||||
|
{message: this.translate.instant('calculated-fields.invalid-file-error'),
|
||||||
|
type: 'error'}));
|
||||||
|
throw new Error('Invalid calculated field file');
|
||||||
|
} else {
|
||||||
|
return this.calculatedFieldsService.saveCalculatedField(this.prepareImport({ entityId, ...calculatedField }));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
catchError(() => of(null)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public exportDashboard(dashboardId: string) {
|
public exportDashboard(dashboardId: string) {
|
||||||
this.getIncludeResourcesPreference('includeResourcesInExportDashboard').subscribe(includeResources => {
|
this.getIncludeResourcesPreference('includeResourcesInExportDashboard').subscribe(includeResources => {
|
||||||
this.openExportDialog('dashboard.export', 'dashboard.export-prompt', includeResources).subscribe(result => {
|
this.openExportDialog('dashboard.export', 'dashboard.export-prompt', includeResources).subscribe(result => {
|
||||||
@ -957,6 +989,17 @@ export class ImportExportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private validateImportedCalculatedField(calculatedField: CalculatedField): boolean {
|
||||||
|
const { name, configuration, entityId } = calculatedField;
|
||||||
|
return isNotEmptyStr(name)
|
||||||
|
&& isDefined(configuration)
|
||||||
|
&& isDefined(entityId?.id)
|
||||||
|
&& !!Object.keys(configuration.arguments).length
|
||||||
|
&& isDefined(configuration.expression)
|
||||||
|
&& isDefined(configuration.output)
|
||||||
|
&& isNotEmptyStr(configuration.output.name);
|
||||||
|
}
|
||||||
|
|
||||||
private validateImportedImage(image: ImageExportData): boolean {
|
private validateImportedImage(image: ImageExportData): boolean {
|
||||||
return !(!isNotEmptyStr(image.data)
|
return !(!isNotEmptyStr(image.data)
|
||||||
|| !isNotEmptyStr(image.title)
|
|| !isNotEmptyStr(image.title)
|
||||||
@ -1209,6 +1252,11 @@ export class ImportExportService {
|
|||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private prepareCalculatedFieldExport(calculatedField: CalculatedField): CalculatedField {
|
||||||
|
delete calculatedField.entityId;
|
||||||
|
return this.prepareExport(calculatedField);
|
||||||
|
}
|
||||||
|
|
||||||
private prepareExport(data: any): any {
|
private prepareExport(data: any): any {
|
||||||
const exportedData = deepClone(data);
|
const exportedData = deepClone(data);
|
||||||
if (isDefined(exportedData.id)) {
|
if (isDefined(exportedData.id)) {
|
||||||
|
|||||||
@ -15,16 +15,15 @@
|
|||||||
///
|
///
|
||||||
|
|
||||||
import { EntityDebugSettings, HasTenantId, HasVersion } from '@shared/models/entity.models';
|
import { EntityDebugSettings, HasTenantId, HasVersion } from '@shared/models/entity.models';
|
||||||
import { BaseData } from '@shared/models/base-data';
|
import { BaseData, ExportableEntity } from '@shared/models/base-data';
|
||||||
import { CalculatedFieldId } from '@shared/models/id/calculated-field-id';
|
import { CalculatedFieldId } from '@shared/models/id/calculated-field-id';
|
||||||
import { EntityId } from '@shared/models/id/entity-id';
|
import { EntityId } from '@shared/models/id/entity-id';
|
||||||
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
|
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
|
||||||
import { EntityType } from '@shared/models/entity-type.models';
|
import { EntityType } from '@shared/models/entity-type.models';
|
||||||
import { AliasFilterType } from '@shared/models/alias.models';
|
import { AliasFilterType } from '@shared/models/alias.models';
|
||||||
|
|
||||||
export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasTenantId {
|
export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasTenantId, ExportableEntity<CalculatedFieldId> {
|
||||||
debugSettings?: EntityDebugSettings;
|
debugSettings?: EntityDebugSettings;
|
||||||
externalId?: string;
|
|
||||||
configuration: CalculatedFieldConfiguration;
|
configuration: CalculatedFieldConfiguration;
|
||||||
type: CalculatedFieldType;
|
type: CalculatedFieldType;
|
||||||
entityId: EntityId;
|
entityId: EntityId;
|
||||||
@ -46,6 +45,13 @@ export interface CalculatedFieldConfiguration {
|
|||||||
type: CalculatedFieldType;
|
type: CalculatedFieldType;
|
||||||
expression: string;
|
expression: string;
|
||||||
arguments: Record<string, CalculatedFieldArgument>;
|
arguments: Record<string, CalculatedFieldArgument>;
|
||||||
|
output: CalculatedFieldOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalculatedFieldOutput {
|
||||||
|
type: OutputType;
|
||||||
|
name: string;
|
||||||
|
scope?: AttributeScope;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ArgumentEntityType {
|
export enum ArgumentEntityType {
|
||||||
|
|||||||
@ -1043,6 +1043,12 @@
|
|||||||
"asset-name": "Asset name",
|
"asset-name": "Asset name",
|
||||||
"timeseries": "Time series",
|
"timeseries": "Time series",
|
||||||
"output": "Output",
|
"output": "Output",
|
||||||
|
"create": "Create new calculated field",
|
||||||
|
"file": "Calculated field file",
|
||||||
|
"invalid-file-error": "Invalid file format. Please make sure the file is a valid JSON file.",
|
||||||
|
"import": "Import calculated field",
|
||||||
|
"export": "Export calculated field",
|
||||||
|
"export-failed-error": "Unable to export calculated field: {{error}}",
|
||||||
"output-type": "Output type",
|
"output-type": "Output type",
|
||||||
"delete-title": "Are you sure you want to delete the calculated field '{{title}}'?",
|
"delete-title": "Are you sure you want to delete the calculated field '{{title}}'?",
|
||||||
"delete-text": "Be careful, after the confirmation the calculated field and all related data will become unrecoverable.",
|
"delete-text": "Be careful, after the confirmation the calculated field and all related data will become unrecoverable.",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user