diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 63be9a437a..b83d029d95 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -49,6 +49,7 @@ public class ControllerConstants { protected static final String RULE_NODE_ID_PARAM_DESCRIPTION = "A string value representing the rule node id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String WIDGET_BUNDLE_ID_PARAM_DESCRIPTION = "A string value representing the widget bundle id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String WIDGET_TYPE_ID_PARAM_DESCRIPTION = "A string value representing the widget type id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String VC_REQUEST_ID_PARAM_DESCRIPTION = "A string value representing the version control request id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String RESOURCE_ID_PARAM_DESCRIPTION = "A string value representing the resource id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String SYSTEM_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'SYS_ADMIN' authority."; protected static final String SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'SYS_ADMIN' or 'TENANT_ADMIN' authority."; diff --git a/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java index 701719af58..1bcc304548 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java @@ -43,7 +43,6 @@ import org.thingsboard.server.common.data.sync.vc.EntityDataDiff; import org.thingsboard.server.common.data.sync.vc.EntityDataInfo; import org.thingsboard.server.common.data.sync.vc.EntityVersion; import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; -import org.thingsboard.server.common.data.sync.vc.EntityTypeLoadResult; import org.thingsboard.server.common.data.sync.vc.VersionLoadResult; import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo; import org.thingsboard.server.common.data.sync.vc.request.create.VersionCreateRequest; @@ -65,6 +64,7 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VERSI import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.VC_REQUEST_ID_PARAM_DESCRIPTION; @RestController @TbCoreComponent @@ -117,14 +117,18 @@ public class EntitiesVersionControlController extends BaseController { " }\n" + "}\n```") @PostMapping("/version") - public DeferredResult saveEntitiesVersion(@RequestBody VersionCreateRequest request) throws ThingsboardException { + public DeferredResult saveEntitiesVersion(@RequestBody VersionCreateRequest request) throws Exception { SecurityUser user = getCurrentUser(); - try { - accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE); - return wrapFuture(versionControlService.saveEntitiesVersion(user, request)); - } catch (Exception e) { - throw handleException(e); - } + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE); + return wrapFuture(versionControlService.saveEntitiesVersion(user, request)); + } + + @ApiOperation(value = "", notes = "") + @GetMapping(value = "/version/{requestId}/status") + public VersionCreationResult getVersionCreateRequestStatus(@ApiParam(value = VC_REQUEST_ID_PARAM_DESCRIPTION, required = true) + @PathVariable UUID requestId) throws Exception { + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); + return versionControlService.getVersionCreateStatus(getCurrentUser(), requestId); } @ApiOperation(value = "", notes = "" + @@ -147,15 +151,11 @@ public class EntitiesVersionControlController extends BaseController { @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp") @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) - @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); - EntityId externalEntityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return wrapFuture(versionControlService.listEntityVersions(getTenantId(), branch, externalEntityId, pageLink)); - } catch (Exception e) { - throw handleException(e); - } + @RequestParam(required = false) String sortOrder) throws Exception { + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); + EntityId externalEntityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return wrapFuture(versionControlService.listEntityVersions(getTenantId(), branch, externalEntityId, pageLink)); } @ApiOperation(value = "", notes = "" + @@ -177,14 +177,10 @@ public class EntitiesVersionControlController extends BaseController { @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp") @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) - @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return wrapFuture(versionControlService.listEntityTypeVersions(getTenantId(), branch, entityType, pageLink)); - } catch (Exception e) { - throw handleException(e); - } + @RequestParam(required = false) String sortOrder) throws Exception { + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return wrapFuture(versionControlService.listEntityTypeVersions(getTenantId(), branch, entityType, pageLink)); } @ApiOperation(value = "", notes = "" + @@ -213,65 +209,45 @@ public class EntitiesVersionControlController extends BaseController { @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp") @RequestParam(required = false) String sortProperty, @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) - @RequestParam(required = false) String sortOrder) throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); - PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return wrapFuture(versionControlService.listVersions(getTenantId(), branch, pageLink)); - } catch (Exception e) { - throw handleException(e); - } + @RequestParam(required = false) String sortOrder) throws Exception { + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return wrapFuture(versionControlService.listVersions(getTenantId(), branch, pageLink)); } @GetMapping("/entity/{branch}/{entityType}/{versionId}") public DeferredResult> listEntitiesAtVersion(@PathVariable String branch, @PathVariable EntityType entityType, - @PathVariable String versionId) throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); - return wrapFuture(versionControlService.listEntitiesAtVersion(getTenantId(), branch, versionId, entityType)); - } catch (Exception e) { - throw handleException(e); - } + @PathVariable String versionId) throws Exception { + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); + return wrapFuture(versionControlService.listEntitiesAtVersion(getTenantId(), branch, versionId, entityType)); } @GetMapping("/entity/{branch}/{versionId}") public DeferredResult> listAllEntitiesAtVersion(@PathVariable String branch, - @PathVariable String versionId) throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); - return wrapFuture(versionControlService.listAllEntitiesAtVersion(getTenantId(), branch, versionId)); - } catch (Exception e) { - throw handleException(e); - } + @PathVariable String versionId) throws Exception { + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); + return wrapFuture(versionControlService.listAllEntitiesAtVersion(getTenantId(), branch, versionId)); } @GetMapping("/info/{versionId}/{entityType}/{externalEntityUuid}") public DeferredResult getEntityDataInfo(@PathVariable String versionId, @PathVariable EntityType entityType, - @PathVariable UUID externalEntityUuid) throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); - EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid); - return wrapFuture(versionControlService.getEntityDataInfo(getCurrentUser(), entityId, versionId)); - } catch (Exception e) { - throw handleException(e); - } + @PathVariable UUID externalEntityUuid) throws Exception { + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid); + return wrapFuture(versionControlService.getEntityDataInfo(getCurrentUser(), entityId, versionId)); } @GetMapping("/diff/{branch}/{entityType}/{internalEntityUuid}") public DeferredResult compareEntityDataToVersion(@PathVariable String branch, @PathVariable EntityType entityType, @PathVariable UUID internalEntityUuid, - @RequestParam String versionId) throws ThingsboardException { - try { - accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); - EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, internalEntityUuid); - return wrapFuture(versionControlService.compareEntityDataToVersion(getCurrentUser(), branch, entityId, versionId)); - } catch (Exception e) { - throw handleException(e); - } + @RequestParam String versionId) throws Exception { + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, internalEntityUuid); + return wrapFuture(versionControlService.compareEntityDataToVersion(getCurrentUser(), branch, entityId, versionId)); } @ApiOperation(value = "", notes = "" + @@ -307,14 +283,18 @@ public class EntitiesVersionControlController extends BaseController { " }\n" + "}\n```") @PostMapping("/entity") - public DeferredResult loadEntitiesVersion(@RequestBody VersionLoadRequest request) throws ThingsboardException { + public UUID loadEntitiesVersion(@RequestBody VersionLoadRequest request) throws Exception { SecurityUser user = getCurrentUser(); - try { - accessControlService.checkPermission(user, Resource.VERSION_CONTROL, Operation.READ); - return wrapFuture(versionControlService.loadEntitiesVersion(user, request)); - } catch (Exception e) { - throw handleException(e); - } + accessControlService.checkPermission(user, Resource.VERSION_CONTROL, Operation.WRITE); + return versionControlService.loadEntitiesVersion(user, request); + } + + @ApiOperation(value = "", notes = "") + @GetMapping(value = "/entity/{requestId}/status") + public VersionLoadResult getVersionLoadRequestStatus(@ApiParam(value = VC_REQUEST_ID_PARAM_DESCRIPTION, required = true) + @PathVariable UUID requestId) throws Exception { + accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); + return versionControlService.getVersionLoadStatus(getCurrentUser(), requestId); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java index 1f6c174fdc..6ea8115e53 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java @@ -25,9 +25,13 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.web.context.request.async.DeferredResult; +import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.TbStopWatch; import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.StringUtils; @@ -73,6 +77,7 @@ import org.thingsboard.server.service.sync.ie.EntitiesExportImportService; import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService; import org.thingsboard.server.service.sync.ie.importing.impl.MissingEntityException; import org.thingsboard.server.service.sync.vc.autocommit.TbAutoCommitSettingsService; +import org.thingsboard.server.service.sync.vc.data.CommitGitRequest; import org.thingsboard.server.service.sync.vc.data.ComplexEntitiesExportCtx; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; @@ -110,6 +115,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont private final ExportableEntitiesService exportableEntitiesService; private final TbNotificationEntityService entityNotificationService; private final TransactionTemplate transactionTemplate; + private final TbTransactionalCache taskCache; private ListeningExecutorService executor; @@ -130,23 +136,55 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont @SuppressWarnings("UnstableApiUsage") @Override - public ListenableFuture saveEntitiesVersion(SecurityUser user, VersionCreateRequest request) throws Exception { + public ListenableFuture saveEntitiesVersion(SecurityUser user, VersionCreateRequest request) throws Exception { var pendingCommit = gitServiceQueue.prepareCommit(user, request); - - return transformAsync(pendingCommit, commit -> { - List> gitFutures = new ArrayList<>(); - switch (request.getType()) { - case SINGLE_ENTITY: { - handleSingleEntityRequest(new SimpleEntitiesExportCtx(user, commit, (SingleEntityVersionCreateRequest) request)); - break; - } - case COMPLEX: { - handleComplexRequest(new ComplexEntitiesExportCtx(user, commit, (ComplexVersionCreateRequest) request)); - break; + DonAsynchron.withCallback(pendingCommit, commit -> { + cachePut(commit.getTxId(), new VersionCreationResult()); + try { + List> gitFutures = new ArrayList<>(); + switch (request.getType()) { + case SINGLE_ENTITY: { + handleSingleEntityRequest(new SimpleEntitiesExportCtx(user, commit, (SingleEntityVersionCreateRequest) request)); + break; + } + case COMPLEX: { + handleComplexRequest(new ComplexEntitiesExportCtx(user, commit, (ComplexVersionCreateRequest) request)); + break; + } } + var resultFuture = Futures.transformAsync(Futures.allAsList(gitFutures), f -> gitServiceQueue.push(commit), executor); + DonAsynchron.withCallback(resultFuture, result -> cachePut(commit.getTxId(), result), e -> processCommitError(user, request, commit, e), executor); + } catch (Exception e) { + processCommitError(user, request, commit, e); } - return transformAsync(Futures.allAsList(gitFutures), success -> gitServiceQueue.push(commit), executor); - }, executor); + }, t -> log.debug("[{}] Failed to prepare the commit: {}", user.getId(), request, t)); + + return transform(pendingCommit, CommitGitRequest::getTxId, MoreExecutors.directExecutor()); + } + + @Override + public VersionCreationResult getVersionCreateStatus(SecurityUser user, UUID requestId) throws ThingsboardException { + return getStatus(user, requestId, VersionControlTaskCacheEntry::getExportResult); + } + + @Override + public VersionLoadResult getVersionLoadStatus(SecurityUser user, UUID requestId) throws ThingsboardException { + return getStatus(user, requestId, VersionControlTaskCacheEntry::getImportResult); + } + + private T getStatus(SecurityUser user, UUID requestId, Function getter) throws ThingsboardException { + var cacheEntry = taskCache.get(requestId); + if (cacheEntry == null || cacheEntry.get() == null) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } else { + var entry = cacheEntry.get(); + var result = getter.apply(entry); + if (result == null) { + throw new ThingsboardException(ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } else { + return result; + } + } } private void handleSingleEntityRequest(SimpleEntitiesExportCtx ctx) throws Exception { @@ -214,40 +252,44 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont @SuppressWarnings({"UnstableApiUsage", "rawtypes"}) @Override - public ListenableFuture loadEntitiesVersion(SecurityUser user, VersionLoadRequest request) throws Exception { + public UUID loadEntitiesVersion(SecurityUser user, VersionLoadRequest request) throws Exception { + EntitiesImportCtx ctx = new EntitiesImportCtx(UUID.randomUUID(), user, request.getVersionId()); + cachePut(ctx.getRequestId(), VersionLoadResult.empty()); switch (request.getType()) { case SINGLE_ENTITY: { SingleEntityVersionLoadRequest versionLoadRequest = (SingleEntityVersionLoadRequest) request; VersionLoadConfig config = versionLoadRequest.getConfig(); ListenableFuture future = gitServiceQueue.getEntity(user.getTenantId(), request.getVersionId(), versionLoadRequest.getExternalEntityId()); - return Futures.transform(future, entityData -> doInTemplate(user, request, ctx -> loadSingleEntity(ctx, config, entityData)), executor); + DonAsynchron.withCallback(future, + entityData -> doInTemplate(ctx, request, c -> loadSingleEntity(c, config, entityData)), + e -> processLoadError(ctx, e), executor); + break; } case ENTITY_TYPE: { EntityTypeVersionLoadRequest versionLoadRequest = (EntityTypeVersionLoadRequest) request; - return executor.submit(() -> doInTemplate(user, request, ctx -> loadMultipleEntities(ctx, versionLoadRequest))); + executor.submit(() -> doInTemplate(ctx, request, c -> loadMultipleEntities(c, versionLoadRequest))); + break; } default: throw new IllegalArgumentException("Unsupported version load request"); } + + return ctx.getRequestId(); } - private VersionLoadResult doInTemplate(SecurityUser user, VersionLoadRequest request, Function function) { + private VersionLoadResult doInTemplate(EntitiesImportCtx ctx, VersionLoadRequest request, Function function) { try { - EntitiesImportCtx ctx = new EntitiesImportCtx(user, request.getVersionId()); VersionLoadResult result = transactionTemplate.execute(status -> function.apply(ctx)); - try { - for (ThrowingRunnable throwingRunnable : ctx.getEventCallbacks()) { - throwingRunnable.run(); - } - } catch (ThingsboardException e) { - throw new RuntimeException(e); + for (ThrowingRunnable throwingRunnable : ctx.getEventCallbacks()) { + throwingRunnable.run(); } - return result; + result.setDone(true); + return cachePut(ctx.getRequestId(), result); } catch (LoadEntityException e) { - return onError(e.getData(), e.getCause()); + return cachePut(ctx.getRequestId(), onError(e.getData(), e.getCause())); } catch (Exception e) { - log.info("[{}] Failed to process request [{}] due to: ", user.getTenantId(), request, e); - throw e; + log.info("[{}] Failed to process request [{}] due to: ", ctx.getTenantId(), request, e); + return cachePut(ctx.getRequestId(), VersionLoadResult.error(EntityLoadError.runtimeError(e.getMessage()))); } } @@ -286,19 +328,21 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont sw.startNew("Entities " + entityType.name()); ctx.setSettings(getEntityImportSettings(request, entityType)); importEntities(ctx, entityType); + persistToCache(ctx); } sw.startNew("Reimport"); reimport(ctx); + persistToCache(ctx); sw.startNew("Remove Others"); request.getEntityTypes().keySet().stream() .filter(entityType -> request.getEntityTypes().get(entityType).isRemoveOtherEntities()) .sorted(exportImportService.getEntityTypeComparatorForImport().reversed()) .forEach(entityType -> removeOtherEntities(ctx, entityType)); + persistToCache(ctx); sw.startNew("References and Relations"); - exportImportService.saveReferencesAndRelations(ctx); sw.stop(); @@ -387,7 +431,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont } private VersionLoadResult onError(EntityExportData entityData, Throwable e) { - return analyze(e, entityData).orElseThrow(() -> new RuntimeException(e)); + return analyze(e, entityData).orElse(VersionLoadResult.error(EntityLoadError.runtimeError(e.getMessage()))); } private Optional analyze(Throwable e, EntityExportData entityData) { @@ -477,7 +521,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont } @Override - public ListenableFuture autoCommit(SecurityUser user, EntityId entityId) throws Exception { + public ListenableFuture autoCommit(SecurityUser user, EntityId entityId) throws Exception { var repositorySettings = repositorySettingsService.get(user.getTenantId()); if (repositorySettings == null) { return Futures.immediateFuture(null); @@ -504,7 +548,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont } @Override - public ListenableFuture autoCommit(SecurityUser user, EntityType entityType, List entityIds) throws Exception { + public ListenableFuture autoCommit(SecurityUser user, EntityType entityType, List entityIds) throws Exception { var repositorySettings = repositorySettingsService.get(user.getTenantId()); if (repositorySettings == null) { return Futures.immediateFuture(null); @@ -550,5 +594,28 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont } } + private void processCommitError(SecurityUser user, VersionCreateRequest request, CommitGitRequest commit, Throwable e) { + log.debug("[{}] Failed to prepare the commit: {}", user.getId(), request, e); + cachePut(commit.getTxId(), new VersionCreationResult(e.getMessage())); + } + + private void processLoadError(EntitiesImportCtx ctx, Throwable e) { + log.debug("[{}] Failed to load the commit: {}", ctx.getRequestId(), ctx.getVersionId(), e); + cachePut(ctx.getRequestId(), VersionLoadResult.error(EntityLoadError.runtimeError(e.getMessage()))); + } + + private void cachePut(UUID requestId, VersionCreationResult result) { + taskCache.put(requestId, VersionControlTaskCacheEntry.newForExport(result)); + } + + private VersionLoadResult cachePut(UUID requestId, VersionLoadResult result) { + log.debug("[{}] Cache put: {}", requestId, result); + taskCache.put(requestId, VersionControlTaskCacheEntry.newForImport(result)); + return result; + } + + private void persistToCache(EntitiesImportCtx ctx) { + cachePut(ctx.getRequestId(), VersionLoadResult.success(new ArrayList<>(ctx.getResults().values()))); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java index f5585fac39..99246d282f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java @@ -378,6 +378,7 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu commitResult.setAdded(commitResponse.getAdded()); commitResult.setRemoved(commitResponse.getRemoved()); commitResult.setModified(commitResponse.getModified()); + commitResult.setDone(true); ((CommitGitRequest) request).getFuture().set(commitResult); } else if (vcResponseMsg.hasListBranchesResponse()) { var listBranchesResponse = vcResponseMsg.getListBranchesResponse(); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java index 0a0f56eac6..3ae68e33d0 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java @@ -16,7 +16,9 @@ package org.thingsboard.server.service.sync.vc; import com.google.common.util.concurrent.ListenableFuture; +import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -38,7 +40,9 @@ import java.util.UUID; public interface EntitiesVersionControlService { - ListenableFuture saveEntitiesVersion(SecurityUser user, VersionCreateRequest request) throws Exception; + ListenableFuture saveEntitiesVersion(SecurityUser user, VersionCreateRequest request) throws Exception; + + VersionCreationResult getVersionCreateStatus(SecurityUser user, UUID requestId) throws ThingsboardException; ListenableFuture> listEntityVersions(TenantId tenantId, String branch, EntityId externalId, PageLink pageLink) throws Exception; @@ -50,7 +54,9 @@ public interface EntitiesVersionControlService { ListenableFuture> listAllEntitiesAtVersion(TenantId tenantId, String branch, String versionId) throws Exception; - ListenableFuture loadEntitiesVersion(SecurityUser user, VersionLoadRequest request) throws Exception; + UUID loadEntitiesVersion(SecurityUser user, VersionLoadRequest request) throws Exception; + + VersionLoadResult getVersionLoadStatus(SecurityUser user, UUID requestId) throws ThingsboardException; ListenableFuture compareEntityDataToVersion(SecurityUser user, String branch, EntityId entityId, String versionId) throws Exception; @@ -64,9 +70,10 @@ public interface EntitiesVersionControlService { ListenableFuture checkVersionControlAccess(TenantId tenantId, RepositorySettings settings) throws Exception; - ListenableFuture autoCommit(SecurityUser user, EntityId entityId) throws Exception; + ListenableFuture autoCommit(SecurityUser user, EntityId entityId) throws Exception; - ListenableFuture autoCommit(SecurityUser user, EntityType entityType, List entityIds) throws Exception; + ListenableFuture autoCommit(SecurityUser user, EntityType entityType, List entityIds) throws Exception; ListenableFuture getEntityDataInfo(SecurityUser user, EntityId entityId, String versionId); + } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskCacheEntry.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskCacheEntry.java new file mode 100644 index 0000000000..dcf3ef3a70 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskCacheEntry.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.vc; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; +import org.thingsboard.server.common.data.sync.vc.VersionLoadResult; + +import java.io.Serializable; + +@Data +@AllArgsConstructor +public class VersionControlTaskCacheEntry implements Serializable { + + private static final long serialVersionUID = -7875992200801588119L; + + private VersionCreationResult exportResult; + private VersionLoadResult importResult; + + public static VersionControlTaskCacheEntry newForExport(VersionCreationResult result) { + return new VersionControlTaskCacheEntry(result, null); + } + + public static VersionControlTaskCacheEntry newForImport(VersionLoadResult result) { + return new VersionControlTaskCacheEntry(null, result); + } + + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskCaffeineCache.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskCaffeineCache.java new file mode 100644 index 0000000000..e58ceba8c5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskCaffeineCache.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.vc; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.UUID; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("VersionControlTaskCache") +public class VersionControlTaskCaffeineCache extends CaffeineTbTransactionalCache { + + public VersionControlTaskCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.VERSION_CONTROL_TASK_CACHE); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskRedisCache.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskRedisCache.java new file mode 100644 index 0000000000..be053f6308 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskRedisCache.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.vc; + +import com.google.protobuf.InvalidProtocolBufferException; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.UUID; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("VersionControlTaskCache") +public class VersionControlTaskRedisCache extends RedisTbTransactionalCache { + + public VersionControlTaskRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.VERSION_CONTROL_TASK_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>()); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java index c781f3cd18..a5b4b7e70d 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java @@ -35,11 +35,13 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; @Slf4j @Data public class EntitiesImportCtx { + private final UUID requestId; private final SecurityUser user; private final String versionId; @@ -57,11 +59,12 @@ public class EntitiesImportCtx { private EntityImportSettings settings; private EntityImportResult currentImportResult; - public EntitiesImportCtx(SecurityUser user, String versionId) { - this(user, versionId, null); + public EntitiesImportCtx(UUID requestId, SecurityUser user, String versionId) { + this(requestId, user, versionId, null); } - public EntitiesImportCtx(SecurityUser user, String versionId, EntityImportSettings settings) { + public EntitiesImportCtx(UUID requestId, SecurityUser user, String versionId, EntityImportSettings settings) { + this.requestId = requestId; this.user = user; this.versionId = versionId; this.settings = settings; diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 72ed590c4f..07233bb241 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -424,6 +424,9 @@ cache: twoFaVerificationCodes: timeToLiveInMinutes: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_TTL:60}" maxSize: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_MAX_SIZE:100000}" + versionControlTask: + timeToLiveInMinutes: "${CACHE_SPECS_VERSION_CONTROL_TASK_TTL:5}" + maxSize: "${CACHE_SPECS_VERSION_CONTROL_TASK_MAX_SIZE:100000}" redis: # standalone or cluster diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java index 9b34eaae25..e516d5e131 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java @@ -82,6 +82,7 @@ import org.thingsboard.server.service.sync.vc.data.SimpleEntitiesExportCtx; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Collections; +import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -433,7 +434,7 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest } protected , I extends EntityId> EntityImportResult importEntity(User user, EntityExportData exportData, EntityImportSettings importSettings) throws Exception { - EntitiesImportCtx ctx = new EntitiesImportCtx(getSecurityUser(user), null, importSettings); + EntitiesImportCtx ctx = new EntitiesImportCtx(UUID.randomUUID(), getSecurityUser(user), null, importSettings); ctx.setFinalImportAttempt(true); exportData = JacksonUtil.treeToValue(JacksonUtil.valueToTree(exportData), EntityExportData.class); EntityImportResult importResult = exportImportService.importEntity(ctx, exportData); diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java index 4f22dff046..37207798fb 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java @@ -47,7 +47,7 @@ public abstract class RedisTbTransactionalCache keySerializer = new StringRedisSerializer(); + private final RedisSerializer keySerializer = StringRedisSerializer.UTF_8; private final RedisSerializer valueSerializer; private final Expiration evictExpiration; private final Expiration cacheTtl; diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java index 664eaa1fb4..9cb139e26a 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java @@ -67,7 +67,7 @@ public class TbCaffeineCacheConfiguration { //SimpleCacheManager is not a bean (will be wrapped), so call initializeCaches manually manager.initializeCaches(); - return new TransactionAwareCacheManagerProxy(manager); + return manager; } private CaffeineCache buildCache(String name, CacheSpecs cacheSpec) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index ee0d9ee119..c473bcbd6c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -35,4 +35,5 @@ public class CacheConstants { public static final String REPOSITORY_SETTINGS_CACHE = "repositorySettings"; public static final String AUTO_COMMIT_SETTINGS_CACHE = "autoCommitSettings"; public static final String TWO_FA_VERIFICATION_CODES_CACHE = "twoFaVerificationCodes"; + public static final String VERSION_CONTROL_TASK_CACHE = "versionControlTask"; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityLoadError.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityLoadError.java index 900a05e470..bc3475c89e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityLoadError.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityLoadError.java @@ -30,6 +30,7 @@ public class EntityLoadError { private String type; private EntityId source; private EntityId target; + private String message; public static EntityLoadError credentialsError(EntityId sourceId) { return EntityLoadError.builder().type("DEVICE_CREDENTIALS_CONFLICT").source(sourceId).build(); @@ -39,4 +40,8 @@ public class EntityLoadError { return EntityLoadError.builder().type("MISSING_REFERENCED_ENTITY").source(sourceId).target(targetId).build(); } + public static EntityLoadError runtimeError(String msg) { + return EntityLoadError.builder().type("RUNTIME").message(msg).build(); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityVersion.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityVersion.java index c90336d3d7..e7ef81a562 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityVersion.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityVersion.java @@ -19,10 +19,15 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import java.io.Serializable; + @Data @AllArgsConstructor @NoArgsConstructor -public class EntityVersion { +public class EntityVersion implements Serializable { + + private static final long serialVersionUID = -3705022663019175258L; + private long timestamp; private String id; private String name; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/VersionCreationResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/VersionCreationResult.java index 8cb09b1b30..608cc5ddd3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/VersionCreationResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/VersionCreationResult.java @@ -16,11 +16,34 @@ package org.thingsboard.server.common.data.sync.vc; import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; @Data -public class VersionCreationResult { +@NoArgsConstructor +public class VersionCreationResult implements Serializable { + private static final long serialVersionUID = 8032189124530267838L; + private EntityVersion version; private int added; private int modified; private int removed; + + private String error; + private boolean done; + + + public VersionCreationResult(EntityVersion version, int added, int modified, int removed) { + this.version = version; + this.added = added; + this.modified = modified; + this.removed = removed; + this.done = true; + } + + public VersionCreationResult(String error) { + this.error = error; + this.done = true; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/VersionLoadResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/VersionLoadResult.java index 736de26079..05d96f10ce 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/VersionLoadResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/VersionLoadResult.java @@ -19,15 +19,24 @@ import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Builder; import lombok.Data; +import java.io.Serializable; +import java.util.Collections; import java.util.List; @Data @Builder @JsonInclude(JsonInclude.Include.NON_NULL) -public class VersionLoadResult { +public class VersionLoadResult implements Serializable { + + private static final long serialVersionUID = -1386093599856747449L; private List result; private EntityLoadError error; + private boolean done; + + public static VersionLoadResult empty() { + return VersionLoadResult.builder().result(Collections.emptyList()).build(); + } public static VersionLoadResult success(List result) { return VersionLoadResult.builder().result(result).build(); @@ -38,7 +47,7 @@ public class VersionLoadResult { } public static VersionLoadResult error(EntityLoadError error) { - return VersionLoadResult.builder().error(error).build(); + return VersionLoadResult.builder().error(error).done(true).build(); } } diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml index 4e430e5ba6..a97c4cfd37 100644 --- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml +++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml @@ -19,6 +19,8 @@ spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" # If you enabled process metrics you should set 'web-application-type' to 'servlet' value. spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" +spring.main.allow-circular-references: "true" + server: # Server bind address (has no effect if web-environment is disabled). address: "${HTTP_BIND_ADDRESS:0.0.0.0}" diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 635e98d89c..37cc0c5584 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -53,12 +53,14 @@ import { forkJoin, Observable, of, ReplaySubject, Subject, throwError, timer } f import { CancelAnimationFrame } from '@core/services/raf.service'; import { EntityType } from '@shared/models/entity-type.models'; import { - createLabelFromDatasource, createLabelFromPattern, - deepClone, flatFormattedData, + createLabelFromPattern, + deepClone, + flatFormattedData, formattedDataFormDatasourceData, isDefined, isDefinedAndNotNull, - isEqual + isEqual, + parseHttpErrorMessage } from '@core/utils'; import { EntityId } from '@app/shared/models/id/entity-id'; import * as moment_ from 'moment'; @@ -801,10 +803,10 @@ export class WidgetSubscription implements IWidgetSubscription { this.rpcErrorText = 'Request Timeout.'; } else { this.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; - const error = this.extractRejectionErrorText(rejection); + const error = parseHttpErrorMessage(rejection, this.ctx.translate); if (error) { this.rpcErrorText += '
'; - this.rpcErrorText += error; + this.rpcErrorText += error.message; } } this.callbacks.onRpcFailed(this); @@ -816,40 +818,6 @@ export class WidgetSubscription implements IWidgetSubscription { } } - private extractRejectionErrorText(rejection: HttpErrorResponse) { - let error = null; - if (rejection.error) { - error = rejection.error; - try { - error = rejection.error ? JSON.parse(rejection.error) : null; - } catch (e) {} - } - if (error && !error.message) { - error = this.prepareMessageFromData(error); - } else if (error && error.message) { - error = error.message; - } - return error; - } - - private prepareMessageFromData(data) { - if (typeof data === 'object' && data.constructor === ArrayBuffer) { - const msg = String.fromCharCode.apply(null, new Uint8Array(data)); - try { - const msgObj = JSON.parse(msg); - if (msgObj.message) { - return msgObj.message; - } else { - return msg; - } - } catch (e) { - return msg; - } - } else { - return data; - } - } - update(isTimewindowTypeChanged = false) { if (this.type !== widgetType.rpc) { this.widgetTimewindowChangedSubject.next(this.timeWindowConfig); diff --git a/ui-ngx/src/app/core/http/entities-version-control.service.ts b/ui-ngx/src/app/core/http/entities-version-control.service.ts index bb4a10e4e5..ec0b6ba2ae 100644 --- a/ui-ngx/src/app/core/http/entities-version-control.service.ts +++ b/ui-ngx/src/app/core/http/entities-version-control.service.ts @@ -17,13 +17,19 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; -import { Observable, of } from 'rxjs'; +import { Observable, of, timer } from 'rxjs'; import { - BranchInfo, EntityDataDiff, EntityDataInfo, EntityLoadError, entityLoadErrorTranslationMap, EntityLoadErrorType, + BranchInfo, + EntityDataDiff, + EntityDataInfo, + EntityLoadError, + entityLoadErrorTranslationMap, + EntityLoadErrorType, EntityVersion, VersionCreateRequest, VersionCreationResult, - VersionLoadRequest, VersionLoadResult + VersionLoadRequest, + VersionLoadResult } from '@shared/models/vc.models'; import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; @@ -32,9 +38,11 @@ import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.m import { select, Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { selectIsUserLoaded } from '@core/auth/auth.selectors'; -import { catchError, tap } from 'rxjs/operators'; +import { catchError, finalize, map, switchMap, takeWhile, tap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { ActionLoadFinish, ActionLoadStart } from '@core/interceptors/load.actions'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; @Injectable({ providedIn: 'root' @@ -83,16 +91,30 @@ export class EntitiesVersionControlService { } public saveEntitiesVersion(request: VersionCreateRequest, config?: RequestConfig): Observable { - return this.http.post('/api/entities/vc/version', request, defaultHttpOptionsFromConfig(config)).pipe( - tap(() => { + this.store.dispatch(new ActionLoadStart()); + return this.http.post('/api/entities/vc/version', request, + defaultHttpOptionsFromConfig({...config, ...{ignoreLoading: true}})).pipe( + switchMap((requestId) => { + return timer(0, 2000).pipe( + switchMap(() => this.getVersionCreateRequestStatus(requestId, config)), + takeWhile((res) => !res.done, true) + ); + }), + finalize(() => { const branch = request.branch; if (this.branchList && !this.branchList.find(b => b.name === branch)) { this.branchList = null; } - }) + this.store.dispatch(new ActionLoadFinish()); + }), ); } + private getVersionCreateRequestStatus(requestId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/entities/vc/version/${requestId}/status`, + defaultHttpOptionsFromConfig({...config, ...{ignoreLoading: true}})); + } + public listEntityVersions(pageLink: PageLink, branch: string, externalEntityId: EntityId, config?: RequestConfig): Observable> { @@ -114,7 +136,24 @@ export class EntitiesVersionControlService { } public loadEntitiesVersion(request: VersionLoadRequest, config?: RequestConfig): Observable { - return this.http.post('/api/entities/vc/entity', request, defaultHttpOptionsFromConfig(config)); + this.store.dispatch(new ActionLoadStart()); + return this.http.post('/api/entities/vc/entity', request, + defaultHttpOptionsFromConfig({...config, ...{ignoreLoading: true}})).pipe( + switchMap((requestId) => { + return timer(0, 2000).pipe( + switchMap(() => this.getVersionLoadRequestStatus(requestId, config)), + takeWhile((res) => !res.done, true), + ); + }), + finalize(() => { + this.store.dispatch(new ActionLoadFinish()); + }), + ); + } + + private getVersionLoadRequestStatus(requestId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/entities/vc/entity/${requestId}/status`, + defaultHttpOptionsFromConfig({...config, ...{ignoreLoading: true}})); } public compareEntityDataToVersion(branch: string, diff --git a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts index 94968f393a..15c4d3ede3 100644 --- a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts +++ b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts @@ -29,6 +29,7 @@ import { ActionLoadFinish, ActionLoadStart } from './load.actions'; import { ActionNotificationShow } from '@app/core/notification/notification.actions'; import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; +import { parseHttpErrorMessage } from '@core/utils'; let tmpHeaders = {}; @@ -131,43 +132,12 @@ export class GlobalHttpInterceptor implements HttpInterceptor { } if (unhandled && !ignoreErrors) { - let error = null; - if (req.responseType === 'text') { - try { - error = errorResponse.error ? JSON.parse(errorResponse.error) : null; - } catch (e) {} - } else { - error = errorResponse.error; - } - if (error && !error.message) { - this.showError(this.prepareMessageFromData(error)); - } else if (error && error.message) { - this.showError(error.message, error.timeout ? error.timeout : 0); - } else { - this.showError('Unhandled error code ' + (error ? error.status : '\'Unknown\'')); - } + const errorMessageWithTimeout = parseHttpErrorMessage(errorResponse, this.translate, req.responseType); + this.showError(errorMessageWithTimeout.message, errorMessageWithTimeout.timeout); } return throwError(errorResponse); } - private prepareMessageFromData(data) { - if (typeof data === 'object' && data.constructor === ArrayBuffer) { - const msg = String.fromCharCode.apply(null, new Uint8Array(data)); - try { - const msgObj = JSON.parse(msg); - if (msgObj.message) { - return msgObj.message; - } else { - return msg; - } - } catch (e) { - return msg; - } - } else { - return data; - } - } - private retryRequest(req: HttpRequest, next: HttpHandler): Observable> { const thisTimeout = 1000 + Math.random() * 3000; return of(null).pipe( diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 96fad967a1..bedc963186 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -21,6 +21,10 @@ import { Datasource, DatasourceData, FormattedData, ReplaceInfo } from '@app/sha import { EntityId } from '@shared/models/id/entity-id'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { EntityType, baseDetailsPageByEntityType } from '@shared/models/entity-type.models'; +import { HttpErrorResponse } from '@angular/common/http'; +import { letterSpacing } from 'html2canvas/dist/types/css/property-descriptors/letter-spacing'; +import { TranslateService } from '@ngx-translate/core'; +import { serverErrorCodesTranslations } from '@shared/models/constants'; const varsRegex = /\${([^}]*)}/g; @@ -664,3 +668,53 @@ export function randomAlphanumeric(length: number): string { export function getEntityDetailsPageURL(id: string, entityType: EntityType): string { return `${baseDetailsPageByEntityType.get(entityType)}/${id}`; } + +export function parseHttpErrorMessage(errorResponse: HttpErrorResponse, + translate: TranslateService, responseType?: string): {message: string, timeout: number} { + let error = null; + let errorMessage: string; + let timeout = 0; + if (responseType === 'text') { + try { + error = errorResponse.error ? JSON.parse(errorResponse.error) : null; + } catch (e) {} + } else { + error = errorResponse.error; + } + if (error && !error.message) { + errorMessage = prepareMessageFromData(error); + } else if (error && error.message) { + errorMessage = error.message; + timeout = error.timeout ? error.timeout : 0; + } else { + errorMessage = `Unhandled error code ${error ? error.status : '\'Unknown\''}`; + } + if (isObject(errorMessage)) { + let errorText = `${errorResponse.status}: `; + let errorKey = null; + if ((errorMessage as any).errorCode) { + errorKey = serverErrorCodesTranslations.get((errorMessage as any).errorCode); + } + errorText += errorKey ? translate.instant(errorKey) : errorResponse.statusText; + errorMessage = errorText; + } + return {message: errorMessage, timeout}; +} + +function prepareMessageFromData(data): string { + if (typeof data === 'object' && data.constructor === ArrayBuffer) { + const msg = String.fromCharCode.apply(null, new Uint8Array(data)); + try { + const msgObj = JSON.parse(msg); + if (msgObj.message) { + return msgObj.message; + } else { + return msg; + } + } catch (e) { + return msg; + } + } else { + return data; + } +} diff --git a/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html b/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html index 9e15076788..3e849773c3 100644 --- a/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html @@ -16,7 +16,7 @@ -->
-
+

{{ 'version-control.create-entities-version' | translate }}

@@ -69,9 +69,11 @@
-
-
-
+
+
+
+
+
+ +
+
+ version-control.creating-version + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.ts b/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.ts index bb97bfaedf..5348b4b340 100644 --- a/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { @@ -30,13 +30,16 @@ import { EntitiesVersionControlService } from '@core/http/entities-version-contr import { TranslateService } from '@ngx-translate/core'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { Observable, Subscription } from 'rxjs'; +import { share } from 'rxjs/operators'; +import { parseHttpErrorMessage } from '@core/utils'; @Component({ selector: 'tb-complex-version-create', templateUrl: './complex-version-create.component.html', styleUrls: ['./version-control.scss'] }) -export class ComplexVersionCreateComponent extends PageComponent implements OnInit { +export class ComplexVersionCreateComponent extends PageComponent implements OnInit, OnDestroy { @Input() branch: string; @@ -57,10 +60,16 @@ export class ComplexVersionCreateComponent extends PageComponent implements OnIn resultMessage: SafeHtml; + hasError = false; + versionCreateResult: VersionCreationResult = null; versionCreateBranch: string = null; + versionCreateResult$: Observable; + + private versionCreateResultSubscription: Subscription; + constructor(protected store: Store, private entitiesVersionControlService: EntitiesVersionControlService, private cd: ChangeDetectorRef, @@ -79,6 +88,13 @@ export class ComplexVersionCreateComponent extends PageComponent implements OnIn }); } + ngOnDestroy() { + super.ngOnDestroy(); + if (this.versionCreateResultSubscription) { + this.versionCreateResultSubscription.unsubscribe(); + } + } + cancel(): void { if (this.onClose) { this.onClose(this.versionCreateResult, this.versionCreateBranch); @@ -93,11 +109,20 @@ export class ComplexVersionCreateComponent extends PageComponent implements OnIn entityTypes: this.createVersionFormGroup.get('entityTypes').value, type: VersionCreateRequestType.COMPLEX }; - this.entitiesVersionControlService.saveEntitiesVersion(request).subscribe((result) => { - if (!result.added && !result.modified && !result.removed) { + + this.versionCreateResult$ = this.entitiesVersionControlService.saveEntitiesVersion(request, {ignoreErrors: true}).pipe( + share() + ); + this.cd.detectChanges(); + if (this.popoverComponent) { + this.popoverComponent.updatePosition(); + } + + this.versionCreateResultSubscription = this.versionCreateResult$.subscribe((result) => { + if (result.done && !result.added && !result.modified && !result.removed) { this.resultMessage = this.sanitizer.bypassSecurityTrustHtml(this.translate.instant('version-control.nothing-to-commit')); } else { - this.resultMessage = this.sanitizer.bypassSecurityTrustHtml(this.translate.instant('version-control.version-create-result', + this.resultMessage = this.sanitizer.bypassSecurityTrustHtml(result.error ? result.error : this.translate.instant('version-control.version-create-result', {added: result.added, modified: result.modified, removed: result.removed})); } this.versionCreateResult = result; @@ -106,6 +131,14 @@ export class ComplexVersionCreateComponent extends PageComponent implements OnIn if (this.popoverComponent) { this.popoverComponent.updatePosition(); } + }, + (error) => { + this.hasError = true; + this.resultMessage = this.sanitizer.bypassSecurityTrustHtml(parseHttpErrorMessage(error, this.translate).message); + this.cd.detectChanges(); + if (this.popoverComponent) { + this.popoverComponent.updatePosition(); + } }); } } diff --git a/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html b/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html index e52be641ce..05f16b3e2a 100644 --- a/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html @@ -15,8 +15,8 @@ limitations under the License. --> -
-
+
+

{{ 'version-control.restore-entities-from-version' | translate: {versionName} }}

@@ -48,13 +48,13 @@
-
-
+
+
{{ 'version-control.no-entities-restored' | translate }}
{{ entityTypeLoadResultMessage(entityTypeLoadResult) }}
-
+
+ +
+
+ version-control.restoring-entities-from-version + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.ts b/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.ts index a212b7b2ce..718dc5c2e7 100644 --- a/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { FormBuilder, FormGroup } from '@angular/forms'; import { @@ -28,15 +28,18 @@ import { AppState } from '@core/core.state'; import { EntitiesVersionControlService } from '@core/http/entities-version-control.service'; import { TranslateService } from '@ngx-translate/core'; import { entityTypeTranslations } from '@shared/models/entity-type.models'; -import { SafeHtml } from '@angular/platform-browser'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TbPopoverComponent } from '@shared/components/popover.component'; +import { Observable, Subscription } from 'rxjs'; +import { share } from 'rxjs/operators'; +import { parseHttpErrorMessage } from '@core/utils'; @Component({ selector: 'tb-complex-version-load', templateUrl: './complex-version-load.component.html', styleUrls: ['./version-control.scss'] }) -export class ComplexVersionLoadComponent extends PageComponent implements OnInit { +export class ComplexVersionLoadComponent extends PageComponent implements OnInit, OnDestroy { @Input() branch: string; @@ -61,10 +64,17 @@ export class ComplexVersionLoadComponent extends PageComponent implements OnInit errorMessage: SafeHtml; + hasError = false; + + versionLoadResult$: Observable; + + private versionLoadResultSubscription: Subscription; + constructor(protected store: Store, private entitiesVersionControlService: EntitiesVersionControlService, private cd: ChangeDetectorRef, private translate: TranslateService, + private sanitizer: DomSanitizer, private fb: FormBuilder) { super(store); } @@ -75,6 +85,13 @@ export class ComplexVersionLoadComponent extends PageComponent implements OnInit }); } + ngOnDestroy() { + super.ngOnDestroy(); + if (this.versionLoadResultSubscription) { + this.versionLoadResultSubscription.unsubscribe(); + } + } + entityTypeLoadResultMessage(result: EntityTypeLoadResult): string { const entityType = result.entityType; let message = this.translate.instant(entityTypeTranslations.get(entityType).typePlural) + ': '; @@ -105,7 +122,14 @@ export class ComplexVersionLoadComponent extends PageComponent implements OnInit entityTypes: this.loadVersionFormGroup.get('entityTypes').value, type: VersionLoadRequestType.ENTITY_TYPE }; - this.entitiesVersionControlService.loadEntitiesVersion(request).subscribe((result) => { + this.versionLoadResult$ = this.entitiesVersionControlService.loadEntitiesVersion(request, {ignoreErrors: true}).pipe( + share() + ); + this.cd.detectChanges(); + if (this.popoverComponent) { + this.popoverComponent.updatePosition(); + } + this.versionLoadResultSubscription = this.versionLoadResult$.subscribe((result) => { this.versionLoadResult = result; this.entityTypeLoadResults = (result.result || []).filter(res => res.created || res.updated || res.deleted); if (result.error) { @@ -115,6 +139,14 @@ export class ComplexVersionLoadComponent extends PageComponent implements OnInit if (this.popoverComponent) { this.popoverComponent.updatePosition(); } + }, + (error) => { + this.hasError = true; + this.errorMessage = this.sanitizer.bypassSecurityTrustHtml(parseHttpErrorMessage(error, this.translate).message); + this.cd.detectChanges(); + if (this.popoverComponent) { + this.popoverComponent.updatePosition(); + } }); } } diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html index 270d42d328..e9c6122a46 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html @@ -16,7 +16,7 @@ -->
-
+

{{ 'version-control.create-entity-version' | translate }}

@@ -65,15 +65,27 @@
-
-
{{ resultMessage }}
-
- -
+
+
+
+
{{ resultMessage }}
+
+ +
+
+
+ +
+
+ version-control.creating-version + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts index e3bf44560b..6706d80c71 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { @@ -27,16 +27,18 @@ import { AppState } from '@core/core.state'; import { EntitiesVersionControlService } from '@core/http/entities-version-control.service'; import { EntityId } from '@shared/models/id/entity-id'; import { TranslateService } from '@ngx-translate/core'; -import { Observable, of } from 'rxjs'; +import { Observable, of, Subscription } from 'rxjs'; import { EntityType } from '@shared/models/entity-type.models'; import { TbPopoverComponent } from '@shared/components/popover.component'; +import { share } from 'rxjs/operators'; +import { parseHttpErrorMessage } from '@core/utils'; @Component({ selector: 'tb-entity-version-create', templateUrl: './entity-version-create.component.html', styleUrls: ['./version-control.scss'] }) -export class EntityVersionCreateComponent extends PageComponent implements OnInit { +export class EntityVersionCreateComponent extends PageComponent implements OnInit, OnDestroy { @Input() branch: string; @@ -62,6 +64,10 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni resultMessage: string; + versionCreateResult$: Observable; + + private versionCreateResultSubscription: Subscription; + constructor(protected store: Store, private entitiesVersionControlService: EntitiesVersionControlService, private cd: ChangeDetectorRef, @@ -81,6 +87,13 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni }); } + ngOnDestroy() { + super.ngOnDestroy(); + if (this.versionCreateResultSubscription) { + this.versionCreateResultSubscription.unsubscribe(); + } + } + cancel(): void { if (this.onClose) { this.onClose(null, null); @@ -101,18 +114,34 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni }, type: VersionCreateRequestType.SINGLE_ENTITY }; - this.entitiesVersionControlService.saveEntitiesVersion(request).subscribe((result) => { - if (!result.added && !result.modified) { - this.resultMessage = this.translate.instant('version-control.nothing-to-commit'); - this.cd.detectChanges(); - if (this.popoverComponent) { - this.popoverComponent.updatePosition(); + this.versionCreateResult$ = this.entitiesVersionControlService.saveEntitiesVersion(request, {ignoreErrors: true}).pipe( + share() + ); + this.cd.detectChanges(); + if (this.popoverComponent) { + this.popoverComponent.updatePosition(); + } + + this.versionCreateResultSubscription = this.versionCreateResult$.subscribe((result) => { + if (result.done) { + if (!result.added && !result.modified || result.error) { + this.resultMessage = result.error ? result.error : this.translate.instant('version-control.nothing-to-commit'); + this.cd.detectChanges(); + if (this.popoverComponent) { + this.popoverComponent.updatePosition(); + } + } else if (this.onClose) { + this.onClose(result, request.branch); } - } else if (this.onClose) { - this.onClose(result, request.branch); + } + }, + (error) => { + this.resultMessage = parseHttpErrorMessage(error, this.translate).message; + this.cd.detectChanges(); + if (this.popoverComponent) { + this.popoverComponent.updatePosition(); } }); }); } } - diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html index fa2d1f28ae..b096c1907f 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html @@ -16,7 +16,7 @@ -->
-
+

{{ 'version-control.restore-entity-from-version' | translate: {versionName} }}

@@ -55,15 +55,27 @@
-
-
-
- -
+
+
+
+
+
+ +
+
+
+ +
+
+ version-control.restoring-entity-version + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts index c31030f74b..43d2452ea3 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts @@ -14,12 +14,12 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit, Sanitizer } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { FormBuilder, FormGroup } from '@angular/forms'; import { EntityDataInfo, - SingleEntityVersionLoadRequest, + SingleEntityVersionLoadRequest, VersionCreationResult, VersionLoadRequestType, VersionLoadResult } from '@shared/models/vc.models'; @@ -29,15 +29,17 @@ import { EntitiesVersionControlService } from '@core/http/entities-version-contr import { EntityId } from '@shared/models/id/entity-id'; import { TranslateService } from '@ngx-translate/core'; import { TbPopoverComponent } from '@shared/components/popover.component'; -import { delay } from 'rxjs/operators'; -import { SafeHtml } from '@angular/platform-browser'; +import { delay, share } from 'rxjs/operators'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { Observable, Subscription } from 'rxjs'; +import { parseHttpErrorMessage } from '@core/utils'; @Component({ selector: 'tb-entity-version-restore', templateUrl: './entity-version-restore.component.html', styleUrls: ['./version-control.scss'] }) -export class EntityVersionRestoreComponent extends PageComponent implements OnInit { +export class EntityVersionRestoreComponent extends PageComponent implements OnInit, OnDestroy { @Input() branch: string; @@ -63,10 +65,15 @@ export class EntityVersionRestoreComponent extends PageComponent implements OnIn errorMessage: SafeHtml; + versionLoadResult$: Observable; + + private versionLoadResultSubscription: Subscription; + constructor(protected store: Store, private entitiesVersionControlService: EntitiesVersionControlService, private cd: ChangeDetectorRef, private translate: TranslateService, + private sanitizer: DomSanitizer, private fb: FormBuilder) { super(store); } @@ -86,6 +93,13 @@ export class EntityVersionRestoreComponent extends PageComponent implements OnIn }); } + ngOnDestroy() { + super.ngOnDestroy(); + if (this.versionLoadResultSubscription) { + this.versionLoadResultSubscription.unsubscribe(); + } + } + cancel(): void { if (this.onClose) { this.onClose(null); @@ -104,18 +118,34 @@ export class EntityVersionRestoreComponent extends PageComponent implements OnIn }, type: VersionLoadRequestType.SINGLE_ENTITY }; - this.entitiesVersionControlService.loadEntitiesVersion(request).subscribe((result) => { - if (result.error) { - this.errorMessage = this.entitiesVersionControlService.entityLoadErrorToMessage(result.error); - this.cd.detectChanges(); - if (this.popoverComponent) { - this.popoverComponent.updatePosition(); - } - } else { - if (this.onClose) { - this.onClose(result); + this.versionLoadResult$ = this.entitiesVersionControlService.loadEntitiesVersion(request, {ignoreErrors: true}).pipe( + share() + ); + this.cd.detectChanges(); + if (this.popoverComponent) { + this.popoverComponent.updatePosition(); + } + this.versionLoadResultSubscription = this.versionLoadResult$.subscribe((result) => { + if (result.done) { + if (result.error) { + this.errorMessage = this.entitiesVersionControlService.entityLoadErrorToMessage(result.error); + this.cd.detectChanges(); + if (this.popoverComponent) { + this.popoverComponent.updatePosition(); + } + } else { + if (this.onClose) { + this.onClose(result); + } } } + }, + (error) => { + this.errorMessage = this.sanitizer.bypassSecurityTrustHtml(parseHttpErrorMessage(error, this.translate).message); + this.cd.detectChanges(); + if (this.popoverComponent) { + this.popoverComponent.updatePosition(); + } }); } } diff --git a/ui-ngx/src/app/modules/home/components/vc/version-control.scss b/ui-ngx/src/app/modules/home/components/vc/version-control.scss index 01aaae5455..d0d96be941 100644 --- a/ui-ngx/src/app/modules/home/components/vc/version-control.scss +++ b/ui-ngx/src/app/modules/home/components/vc/version-control.scss @@ -27,5 +27,8 @@ text-align: start; font-weight: 400; } + &.progress { + padding-bottom: 32px; + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts index 5beef7ea8c..b9b294cccd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts @@ -42,7 +42,7 @@ import { import cssjs from '@core/css/css'; import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; -import { hashCode, isDefined, isNumber } from '@core/utils'; +import { hashCode, isDefined, isNumber, parseHttpErrorMessage } from '@core/utils'; import { CollectionViewer, DataSource } from '@angular/cdk/collections'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { @@ -520,7 +520,7 @@ class PersistentDatasource implements DataSource { this.subscription.rpcErrorText = 'Request Timeout.'; } else { this.subscription.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; - const error = this.extractRejectionErrorText(rejection); + const error = parseHttpErrorMessage(rejection, this.translate); if (error) { this.subscription.rpcErrorText += '
'; this.subscription.rpcErrorText += error.message || ''; @@ -534,40 +534,6 @@ class PersistentDatasource implements DataSource { return rpcSubject.asObservable(); } - extractRejectionErrorText(rejection: HttpErrorResponse) { - let error = null; - if (rejection.error) { - error = rejection.error; - try { - error = rejection.error ? JSON.parse(rejection.error) : null; - } catch (e) {} - } - if (error && !error.message) { - error = this.prepareMessageFromData(error); - } else if (error && error.message) { - error = error.message; - } - return error; - } - - prepareMessageFromData(data) { - if (typeof data === 'object' && data.constructor === ArrayBuffer) { - const msg = String.fromCharCode.apply(null, new Uint8Array(data)); - try { - const msgObj = JSON.parse(msg); - if (msgObj.message) { - return msgObj.message; - } else { - return msg; - } - } catch (e) { - return msg; - } - } else { - return data; - } - } - isEmpty(): Observable { return this.persistentSubject.pipe( map((requests) => !requests.length) diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index b746ee71b0..28c08f09a0 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -16,6 +16,7 @@ import { InjectionToken } from '@angular/core'; import { IModulesMap } from '@modules/common/modules-map.models'; +import { EntityType } from '@shared/models/entity-type.models'; export const Constants = { serverErrorCode: { @@ -38,6 +39,20 @@ export const Constants = { } }; +export const serverErrorCodesTranslations = new Map([ + [Constants.serverErrorCode.general, 'server-error.general'], + [Constants.serverErrorCode.authentication, 'server-error.authentication'], + [Constants.serverErrorCode.jwtTokenExpired, 'server-error.jwt-token-expired'], + [Constants.serverErrorCode.tenantTrialExpired, 'server-error.tenant-trial-expired'], + [Constants.serverErrorCode.credentialsExpired, 'server-error.credentials-expired'], + [Constants.serverErrorCode.permissionDenied, 'server-error.permission-denied'], + [Constants.serverErrorCode.invalidArguments, 'server-error.invalid-arguments'], + [Constants.serverErrorCode.badRequestParams, 'server-error.bad-request-params'], + [Constants.serverErrorCode.itemNotFound, 'server-error.item-not-found'], + [Constants.serverErrorCode.tooManyRequests, 'server-error.too-many-requests'], + [Constants.serverErrorCode.tooManyUpdates, 'server-error.too-many-updates'], +]); + export const MediaBreakpoints = { xs: 'screen and (max-width: 599px)', sm: 'screen and (min-width: 600px) and (max-width: 959px)', diff --git a/ui-ngx/src/app/shared/models/vc.models.ts b/ui-ngx/src/app/shared/models/vc.models.ts index 5ef0a18b08..98f66f74a9 100644 --- a/ui-ngx/src/app/shared/models/vc.models.ts +++ b/ui-ngx/src/app/shared/models/vc.models.ts @@ -165,6 +165,8 @@ export interface VersionCreationResult { added: number; modified: number; removed: number; + error: string; + done: boolean; } export interface EntityTypeLoadResult { @@ -195,6 +197,7 @@ export interface EntityLoadError { export interface VersionLoadResult { result: Array; error: EntityLoadError; + done: boolean; } export interface AttributeExportData { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 9e0424eb6c..30c4d78059 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3000,6 +3000,19 @@ "retry-failed-and-timeout-hint": "Retry all failed and timed-out messages from processing pack" } }, + "server-error": { + "general": "General server error", + "authentication": "Authentication error", + "jwt-token-expired": "JWT token expired", + "tenant-trial-expired": "Tenant trial expired", + "credentials-expired": "Credentials expired", + "permission-denied": "Permission denied", + "invalid-arguments": "Invalid arguments", + "bad-request-params": "Bad request params", + "item-not-found": "Item not found", + "too-many-requests": "Too many requests", + "too-many-updates": "Too many updates" + }, "tenant": { "tenant": "Tenant", "tenants": "Tenants", @@ -3307,9 +3320,11 @@ "no-versions-text": "No versions found", "copy-full-version-id": "Copy full version id", "create-version": "Create version", + "creating-version": "Creating version... Please wait", "nothing-to-commit": "No changes to commit", "restore-version": "Restore version", "restore-entity-from-version": "Restore entity from version '{{versionName}}'", + "restoring-entity-version": "Restoring entity version... Please wait", "load-relations": "Load relations", "load-attributes": "Load attributes", "load-credentials": "Load credentials", @@ -3335,6 +3350,7 @@ "remove-other-entities": "Remove other entities", "find-existing-entity-by-name": "Find existing entity by name", "restore-entities-from-version": "Restore entities from version '{{versionName}}'", + "restoring-entities-from-version": "Restoring entities... Please wait", "no-entities-restored": "No entities restored", "created": "{{created}} created", "updated": "{{updated}} updated",