Browse Source

Merge pull request #6779 from thingsboard/feature/vc-progress

VC improved progress indicator
pull/6782/head
Andrew Shvayka 4 years ago
committed by GitHub
parent
commit
681028f70d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  2. 120
      application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java
  3. 133
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
  4. 1
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java
  5. 15
      application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java
  6. 43
      application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskCacheEntry.java
  7. 36
      application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskCaffeineCache.java
  8. 41
      application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlTaskRedisCache.java
  9. 9
      application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java
  10. 3
      application/src/main/resources/thingsboard.yml
  11. 3
      application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java
  12. 2
      common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java
  13. 2
      common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java
  14. 1
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  15. 5
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityLoadError.java
  16. 7
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityVersion.java
  17. 25
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/VersionCreationResult.java
  18. 13
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/VersionLoadResult.java
  19. 2
      msa/vc-executor/src/main/resources/tb-vc-executor.yml
  20. 46
      ui-ngx/src/app/core/api/widget-subscription.ts
  21. 55
      ui-ngx/src/app/core/http/entities-version-control.service.ts
  22. 36
      ui-ngx/src/app/core/interceptors/global-http-interceptor.ts
  23. 54
      ui-ngx/src/app/core/utils.ts
  24. 18
      ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html
  25. 43
      ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.ts
  26. 18
      ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html
  27. 40
      ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.ts
  28. 34
      ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html
  29. 53
      ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts
  30. 34
      ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html
  31. 60
      ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts
  32. 3
      ui-ngx/src/app/modules/home/components/vc/version-control.scss
  33. 38
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts
  34. 15
      ui-ngx/src/app/shared/models/constants.ts
  35. 3
      ui-ngx/src/app/shared/models/vc.models.ts
  36. 16
      ui-ngx/src/assets/locale/locale.constant-en_US.json

1
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.";

120
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<VersionCreationResult> saveEntitiesVersion(@RequestBody VersionCreateRequest request) throws ThingsboardException {
public DeferredResult<UUID> 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<List<VersionedEntityInfo>> 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<List<VersionedEntityInfo>> 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<EntityDataInfo> 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<EntityDataDiff> 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<VersionLoadResult> 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);
}

133
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<UUID, VersionControlTaskCacheEntry> taskCache;
private ListeningExecutorService executor;
@ -130,23 +136,55 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@SuppressWarnings("UnstableApiUsage")
@Override
public ListenableFuture<VersionCreationResult> saveEntitiesVersion(SecurityUser user, VersionCreateRequest request) throws Exception {
public ListenableFuture<UUID> saveEntitiesVersion(SecurityUser user, VersionCreateRequest request) throws Exception {
var pendingCommit = gitServiceQueue.prepareCommit(user, request);
return transformAsync(pendingCommit, commit -> {
List<ListenableFuture<Void>> 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<ListenableFuture<Void>> 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> T getStatus(SecurityUser user, UUID requestId, Function<VersionControlTaskCacheEntry, T> 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<VersionLoadResult> 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<EntityExportData> 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 <R> VersionLoadResult doInTemplate(SecurityUser user, VersionLoadRequest request, Function<EntitiesImportCtx, VersionLoadResult> function) {
private <R> VersionLoadResult doInTemplate(EntitiesImportCtx ctx, VersionLoadRequest request, Function<EntitiesImportCtx, VersionLoadResult> 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<VersionLoadResult> analyze(Throwable e, EntityExportData<?> entityData) {
@ -477,7 +521,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
}
@Override
public ListenableFuture<VersionCreationResult> autoCommit(SecurityUser user, EntityId entityId) throws Exception {
public ListenableFuture<UUID> 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<VersionCreationResult> autoCommit(SecurityUser user, EntityType entityType, List<UUID> entityIds) throws Exception {
public ListenableFuture<UUID> autoCommit(SecurityUser user, EntityType entityType, List<UUID> 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())));
}
}

1
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();

15
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<VersionCreationResult> saveEntitiesVersion(SecurityUser user, VersionCreateRequest request) throws Exception;
ListenableFuture<UUID> saveEntitiesVersion(SecurityUser user, VersionCreateRequest request) throws Exception;
VersionCreationResult getVersionCreateStatus(SecurityUser user, UUID requestId) throws ThingsboardException;
ListenableFuture<PageData<EntityVersion>> listEntityVersions(TenantId tenantId, String branch, EntityId externalId, PageLink pageLink) throws Exception;
@ -50,7 +54,9 @@ public interface EntitiesVersionControlService {
ListenableFuture<List<VersionedEntityInfo>> listAllEntitiesAtVersion(TenantId tenantId, String branch, String versionId) throws Exception;
ListenableFuture<VersionLoadResult> loadEntitiesVersion(SecurityUser user, VersionLoadRequest request) throws Exception;
UUID loadEntitiesVersion(SecurityUser user, VersionLoadRequest request) throws Exception;
VersionLoadResult getVersionLoadStatus(SecurityUser user, UUID requestId) throws ThingsboardException;
ListenableFuture<EntityDataDiff> compareEntityDataToVersion(SecurityUser user, String branch, EntityId entityId, String versionId) throws Exception;
@ -64,9 +70,10 @@ public interface EntitiesVersionControlService {
ListenableFuture<Void> checkVersionControlAccess(TenantId tenantId, RepositorySettings settings) throws Exception;
ListenableFuture<VersionCreationResult> autoCommit(SecurityUser user, EntityId entityId) throws Exception;
ListenableFuture<UUID> autoCommit(SecurityUser user, EntityId entityId) throws Exception;
ListenableFuture<VersionCreationResult> autoCommit(SecurityUser user, EntityType entityType, List<UUID> entityIds) throws Exception;
ListenableFuture<UUID> autoCommit(SecurityUser user, EntityType entityType, List<UUID> entityIds) throws Exception;
ListenableFuture<EntityDataInfo> getEntityDataInfo(SecurityUser user, EntityId entityId, String versionId);
}

43
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);
}
}

36
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<UUID, VersionControlTaskCacheEntry> {
public VersionControlTaskCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.VERSION_CONTROL_TASK_CACHE);
}
}

41
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<UUID, VersionControlTaskCacheEntry> {
public VersionControlTaskRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.VERSION_CONTROL_TASK_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>());
}
}

9
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;

3
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

3
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 <E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(User user, EntityExportData<E> 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<E> importResult = exportImportService.importEntity(ctx, exportData);

2
common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java

@ -47,7 +47,7 @@ public abstract class RedisTbTransactionalCache<K extends Serializable, V extend
@Getter
private final String cacheName;
private final JedisConnectionFactory connectionFactory;
private final RedisSerializer<String> keySerializer = new StringRedisSerializer();
private final RedisSerializer<String> keySerializer = StringRedisSerializer.UTF_8;
private final RedisSerializer<V> valueSerializer;
private final Expiration evictExpiration;
private final Expiration cacheTtl;

2
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) {

1
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";
}

5
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();
}
}

7
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;

25
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;
}
}

13
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<EntityTypeLoadResult> result;
private EntityLoadError error;
private boolean done;
public static VersionLoadResult empty() {
return VersionLoadResult.builder().result(Collections.emptyList()).build();
}
public static VersionLoadResult success(List<EntityTypeLoadResult> 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();
}
}

2
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}"

46
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 += '</br>';
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);

55
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<VersionCreationResult> {
return this.http.post<VersionCreationResult>('/api/entities/vc/version', request, defaultHttpOptionsFromConfig(config)).pipe(
tap(() => {
this.store.dispatch(new ActionLoadStart());
return this.http.post<string>('/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<VersionCreationResult> {
return this.http.get<VersionCreationResult>(`/api/entities/vc/version/${requestId}/status`,
defaultHttpOptionsFromConfig({...config, ...{ignoreLoading: true}}));
}
public listEntityVersions(pageLink: PageLink, branch: string,
externalEntityId: EntityId,
config?: RequestConfig): Observable<PageData<EntityVersion>> {
@ -114,7 +136,24 @@ export class EntitiesVersionControlService {
}
public loadEntitiesVersion(request: VersionLoadRequest, config?: RequestConfig): Observable<VersionLoadResult> {
return this.http.post<VersionLoadResult>('/api/entities/vc/entity', request, defaultHttpOptionsFromConfig(config));
this.store.dispatch(new ActionLoadStart());
return this.http.post<string>('/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<VersionLoadResult> {
return this.http.get<VersionLoadResult>(`/api/entities/vc/entity/${requestId}/status`,
defaultHttpOptionsFromConfig({...config, ...{ignoreLoading: true}}));
}
public compareEntityDataToVersion(branch: string,

36
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<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const thisTimeout = 1000 + Math.random() * 3000;
return of(null).pipe(

54
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;
}
}

18
ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html

@ -16,7 +16,7 @@
-->
<section style="min-width: 800px;">
<section *ngIf="!resultMessage">
<section *ngIf="!versionCreateResult$">
<mat-toolbar>
<h2>{{ 'version-control.create-entities-version' | translate }}</h2>
<span fxFlex></span>
@ -69,9 +69,11 @@
</button>
</div>
</section>
<section *ngIf="resultMessage">
<div class="mat-title vc-result-message" [innerHtml]="resultMessage"></div>
<div fxLayoutAlign="end center" fxLayoutGap="8px">
<section *ngIf="versionCreateResult$">
<section *ngIf="resultMessage">
<div class="mat-title vc-result-message" [innerHtml]="resultMessage"></div>
</section>
<div *ngIf="(versionCreateResult$ | async)?.done || hasError; else progress" fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
@ -79,5 +81,13 @@
{{ 'action.close' | translate }}
</button>
</div>
<ng-template #progress>
<section fxLayout="column" fxLayoutAlign="center center">
<div class="mat-title vc-result-message progress">
<span translate>version-control.creating-version</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</section>
</ng-template>
</section>
</section>

43
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<VersionCreationResult>;
private versionCreateResultSubscription: Subscription;
constructor(protected store: Store<AppState>,
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();
}
});
}
}

18
ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html

@ -15,8 +15,8 @@
limitations under the License.
-->
<section [ngStyle]="versionLoadResult ? {minWidth: '500px'} : {minWidth: '800px'}">
<section *ngIf="!versionLoadResult">
<section [ngStyle]="versionLoadResult$ ? {minWidth: '500px'} : {minWidth: '800px'}">
<section *ngIf="!versionLoadResult$">
<mat-toolbar>
<h2>{{ 'version-control.restore-entities-from-version' | translate: {versionName} }}</h2>
<span fxFlex></span>
@ -48,13 +48,13 @@
</button>
</div>
</section>
<section *ngIf="versionLoadResult">
<div *ngIf="!entityTypeLoadResults.length && !errorMessage" class="mat-title vc-result-message">
<section *ngIf="versionLoadResult$">
<div *ngIf="(versionLoadResult$ | async)?.done && !entityTypeLoadResults?.length && !errorMessage" class="mat-title vc-result-message">
{{ 'version-control.no-entities-restored' | translate }}
</div>
<div *ngIf="errorMessage" class="mat-title vc-result-message error" [innerHTML]="errorMessage"></div>
<div *ngFor="let entityTypeLoadResult of entityTypeLoadResults" class="mat-title vc-result-message">{{ entityTypeLoadResultMessage(entityTypeLoadResult) }}</div>
<div fxLayoutAlign="end center" fxLayoutGap="8px">
<div *ngIf="(versionLoadResult$ | async)?.done || hasError; else progress" fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
@ -62,5 +62,13 @@
{{ 'action.close' | translate }}
</button>
</div>
<ng-template #progress>
<section fxLayout="column" fxLayoutAlign="center center">
<div class="mat-title vc-result-message progress">
<span translate>version-control.restoring-entities-from-version</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</section>
</ng-template>
</section>
</section>

40
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<VersionLoadResult>;
private versionLoadResultSubscription: Subscription;
constructor(protected store: Store<AppState>,
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();
}
});
}
}

34
ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html

@ -16,7 +16,7 @@
-->
<section style="min-width: 400px;">
<section *ngIf="!resultMessage">
<section *ngIf="!versionCreateResult$">
<mat-toolbar>
<h2>{{ 'version-control.create-entity-version' | translate }}</h2>
<span fxFlex></span>
@ -65,15 +65,27 @@
</button>
</div>
</section>
<section *ngIf="resultMessage">
<div class="mat-title vc-result-message">{{ resultMessage }}</div>
<div fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
<section *ngIf="versionCreateResult$">
<section *ngIf="(versionCreateResult$ | async)?.done || resultMessage; else progress">
<section *ngIf="resultMessage">
<div class="mat-title vc-result-message">{{ resultMessage }}</div>
<div fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
</section>
</section>
<ng-template #progress>
<section fxLayout="column" fxLayoutAlign="center center">
<div class="mat-title vc-result-message progress">
<span translate>version-control.creating-version</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</section>
</ng-template>
</section>
</section>

53
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<VersionCreationResult>;
private versionCreateResultSubscription: Subscription;
constructor(protected store: Store<AppState>,
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();
}
});
});
}
}

34
ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html

@ -16,7 +16,7 @@
-->
<section [ngStyle]="entityDataInfo ? {minWidth: '400px'} : {}">
<section *ngIf="!errorMessage">
<section *ngIf="!versionLoadResult$">
<mat-toolbar *ngIf="entityDataInfo">
<h2>{{ 'version-control.restore-entity-from-version' | translate: {versionName} }}</h2>
<span fxFlex></span>
@ -55,15 +55,27 @@
</button>
</div>
</section>
<section *ngIf="errorMessage">
<div class="mat-title vc-result-message error" [innerHTML]="errorMessage"></div>
<div fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
<section *ngIf="versionLoadResult$">
<section *ngIf="(versionLoadResult$ | async)?.done || errorMessage; else progress">
<section *ngIf="errorMessage">
<div class="mat-title vc-result-message error" [innerHTML]="errorMessage"></div>
<div fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
</section>
</section>
<ng-template #progress>
<section fxLayout="column" fxLayoutAlign="center center">
<div class="mat-title vc-result-message progress">
<span translate>version-control.restoring-entity-version</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</section>
</ng-template>
</section>
</section>

60
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<VersionLoadResult>;
private versionLoadResultSubscription: Subscription;
constructor(protected store: Store<AppState>,
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();
}
});
}
}

3
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;
}
}
}

38
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<PersistentRpcData> {
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 += '</br>';
this.subscription.rpcErrorText += error.message || '';
@ -534,40 +534,6 @@ class PersistentDatasource implements DataSource<PersistentRpcData> {
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<boolean> {
return this.persistentSubject.pipe(
map((requests) => !requests.length)

15
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<number, string>([
[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)',

3
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<EntityTypeLoadResult>;
error: EntityLoadError;
done: boolean;
}
export interface AttributeExportData {

16
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",

Loading…
Cancel
Save