Browse Source

Merge remote-tracking branch 'origin/feature/entities-version-control' into entities-vc-refactoring

# Conflicts:
#	common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java
pull/6601/head
Viacheslav Klimov 4 years ago
parent
commit
9c2f96b262
  1. 32
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  2. 2
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  3. 19
      application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java
  4. 23
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
  5. 58
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java
  6. 6
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultTbVersionControlSettingsService.java
  7. 6
      application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java
  8. 3
      application/src/main/resources/thingsboard.yml
  9. 3
      common/cluster-api/src/main/proto/queue.proto
  10. 13
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntitiesVersionControlSettings.java
  11. 12
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java
  12. 48
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java
  13. 3
      msa/vc-executor/src/main/resources/tb-vc-executor.yml
  14. 17
      ui-ngx/src/app/core/http/admin.service.ts
  15. 57
      ui-ngx/src/app/core/http/entities-version-control.service.ts
  16. 1
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  17. 1
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  18. 13
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  19. 6
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
  20. 73
      ui-ngx/src/app/modules/home/components/vc/entity-version-export.component.html
  21. 21
      ui-ngx/src/app/modules/home/components/vc/entity-version-export.component.scss
  22. 96
      ui-ngx/src/app/modules/home/components/vc/entity-version-export.component.ts
  23. 49
      ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html
  24. 86
      ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts
  25. 76
      ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html
  26. 2
      ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.scss
  27. 152
      ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.ts
  28. 77
      ui-ngx/src/app/modules/home/components/vc/vc-entity-export-dialog.component.html
  29. 105
      ui-ngx/src/app/modules/home/components/vc/vc-entity-export-dialog.component.ts
  30. 3
      ui-ngx/src/app/modules/home/components/vc/version-control-settings.component.ts
  31. 4
      ui-ngx/src/app/modules/home/components/vc/version-control.component.html
  32. 8
      ui-ngx/src/app/modules/home/components/vc/version-control.component.ts
  33. 16
      ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts
  34. 6
      ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html
  35. 6
      ui-ngx/src/app/modules/home/pages/asset/asset.component.html
  36. 10
      ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts
  37. 6
      ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.html
  38. 6
      ui-ngx/src/app/modules/home/pages/customer/customer.component.html
  39. 10
      ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts
  40. 6
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html
  41. 6
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-tabs.component.html
  42. 10
      ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts
  43. 6
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html
  44. 10
      ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts
  45. 3
      ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html
  46. 6
      ui-ngx/src/app/modules/home/pages/device/device.component.html
  47. 10
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  48. 6
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.html
  49. 6
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html
  50. 10
      ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts
  51. 3
      ui-ngx/src/app/shared/components/popover.component.ts
  52. 4
      ui-ngx/src/app/shared/components/popover.service.ts
  53. 13
      ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html
  54. 30
      ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.scss
  55. 129
      ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts
  56. 29
      ui-ngx/src/app/shared/models/vc.models.ts
  57. 17
      ui-ngx/src/assets/locale/locale.constant-en_US.json

32
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -16,12 +16,16 @@
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.AdminSettings;
@ -221,17 +225,15 @@ public class AdminController extends BaseController {
notes = "Creates or Updates the version control settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/vcSettings")
public EntitiesVersionControlSettings saveVersionControlSettings(@RequestBody EntitiesVersionControlSettings settings) throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
EntitiesVersionControlSettings versionControlSettings = checkNotNull(versionControlService.saveVersionControlSettings(getTenantId(), settings));
versionControlSettings.setPassword(null);
versionControlSettings.setPrivateKey(null);
versionControlSettings.setPrivateKeyPassword(null);
return versionControlSettings;
} catch (Exception e) {
throw handleException(e);
}
public DeferredResult<EntitiesVersionControlSettings> saveVersionControlSettings(@RequestBody EntitiesVersionControlSettings settings) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
ListenableFuture<EntitiesVersionControlSettings> future = versionControlService.saveVersionControlSettings(getTenantId(), settings);
return wrapFuture(Futures.transform(future, savedSettings -> {
savedSettings.setPassword(null);
savedSettings.setPrivateKey(null);
savedSettings.setPrivateKeyPassword(null);
return savedSettings;
}, MoreExecutors.directExecutor()));
}
@ApiOperation(value = "Delete version control settings (deleteVersionControlSettings)",
@ -240,10 +242,10 @@ public class AdminController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/vcSettings", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void deleteVersionControlSettings() throws ThingsboardException {
public DeferredResult<Void> deleteVersionControlSettings() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.DELETE);
versionControlService.deleteVersionControlSettings(getTenantId());
return wrapFuture(versionControlService.deleteVersionControlSettings(getTenantId()));
} catch (Exception e) {
throw handleException(e);
}
@ -253,13 +255,13 @@ public class AdminController extends BaseController {
notes = "Attempts to check version control access. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/vcSettings/checkAccess", method = RequestMethod.POST)
public void checkVersionControlAccess(
public DeferredResult<Void> checkVersionControlAccess(
@ApiParam(value = "A JSON value representing the Entities Version Control Settings.")
@RequestBody EntitiesVersionControlSettings settings) throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
settings = checkNotNull(settings);
versionControlService.checkVersionControlAccess(getTenantId(), settings);
return wrapFuture(versionControlService.checkVersionControlAccess(getTenantId(), settings));
} catch (Exception e) {
throw handleException(e);
}

2
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -135,6 +135,8 @@ public class ControllerConstants {
protected static final String EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION = "Assignment works in async way - first, notification event pushed to edge service queue on platform. ";
protected static final String EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION = "(Edge will receive this instantly, if it's currently connected, or once it's going to be connected to platform). ";
protected static final String ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the entity version name.";
protected static final String MARKDOWN_CODE_BLOCK_START = "```json\n";
protected static final String MARKDOWN_CODE_BLOCK_END = "\n```";
protected static final String EVENT_ERROR_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START +

19
application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java

@ -54,7 +54,12 @@ import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.*;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION;
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;
@RestController
@ -132,13 +137,15 @@ public class EntitiesVersionControlController extends BaseController {
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@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 {
EntityId externalEntityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid);
PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return wrapFuture(versionControlService.listEntityVersions(getTenantId(), branch, externalEntityId, pageLink));
} catch (Exception e) {
throw handleException(e);
@ -159,12 +166,14 @@ public class EntitiesVersionControlController extends BaseController {
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@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 {
PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return wrapFuture(versionControlService.listEntityTypeVersions(getTenantId(), branch, entityType, pageLink));
} catch (Exception e) {
throw handleException(e);
@ -192,12 +201,14 @@ public class EntitiesVersionControlController extends BaseController {
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@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 {
PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return wrapFuture(versionControlService.listVersions(getTenantId(), branch, pageLink));
} catch (Exception e) {
throw handleException(e);

23
application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java

@ -198,7 +198,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
SingleEntityVersionLoadRequest versionLoadRequest = (SingleEntityVersionLoadRequest) request;
VersionLoadConfig config = versionLoadRequest.getConfig();
ListenableFuture<EntityExportData> future = gitServiceQueue.getEntity(user.getTenantId(), request.getVersionId(), versionLoadRequest.getExternalEntityId());
Futures.transform(future, entityData -> {
return Futures.transform(future, entityData -> {
EntityImportResult<?> importResult = transactionTemplate.execute(status -> {
try {
return exportImportService.importEntity(user, entityData, EntityImportSettings.builder()
@ -336,31 +336,30 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
}
@Override
public EntitiesVersionControlSettings saveVersionControlSettings(TenantId tenantId, EntitiesVersionControlSettings versionControlSettings) {
versionControlSettings = this.vcSettingsService.restore(tenantId, versionControlSettings);
public ListenableFuture<EntitiesVersionControlSettings> saveVersionControlSettings(TenantId tenantId, EntitiesVersionControlSettings versionControlSettings) {
var restoredSettings = this.vcSettingsService.restore(tenantId, versionControlSettings);
try {
//TODO: ashvayka: replace future.get with deferred result. Don't forget to call when tenant is deleted.
gitServiceQueue.initRepository(tenantId, versionControlSettings).get();
var future = gitServiceQueue.initRepository(tenantId, restoredSettings);
return Futures.transform(future, f -> vcSettingsService.save(tenantId, restoredSettings), MoreExecutors.directExecutor());
} catch (Exception e) {
throw new RuntimeException("Failed to init repository!", e);
}
return vcSettingsService.save(tenantId, versionControlSettings);
}
@Override
public void deleteVersionControlSettings(TenantId tenantId) throws Exception {
public ListenableFuture<Void> deleteVersionControlSettings(TenantId tenantId) throws Exception {
if (vcSettingsService.delete(tenantId)) {
//TODO: ashvayka: replace future.get with deferred result. Don't forget to call when tenant is deleted.
gitServiceQueue.clearRepository(tenantId).get();
return gitServiceQueue.clearRepository(tenantId);
} else {
return Futures.immediateFuture(null);
}
}
@Override
public void checkVersionControlAccess(TenantId tenantId, EntitiesVersionControlSettings settings) throws ThingsboardException {
public ListenableFuture<Void> checkVersionControlAccess(TenantId tenantId, EntitiesVersionControlSettings settings) throws ThingsboardException {
settings = this.vcSettingsService.restore(tenantId, settings);
try {
//TODO: ashvayka: replace future.get with deferred result.
gitServiceQueue.testRepository(tenantId, settings).get();
return gitServiceQueue.testRepository(tenantId, settings);
} catch (Exception e) {
throw new ThingsboardException(String.format("Unable to access repository: %s", getCauseMessage(e)),
ThingsboardErrorCode.GENERAL);

58
application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java

@ -150,32 +150,54 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
@Override
public ListenableFuture<PageData<EntityVersion>> listVersions(TenantId tenantId, String branch, PageLink pageLink) {
return listVersions(tenantId, ListVersionsRequestMsg.newBuilder()
.setBranchName(branch)
.setPageSize(pageLink.getPageSize())
.setPage(pageLink.getPage())
.build());
return listVersions(tenantId,
applyPageLinkParameters(
ListVersionsRequestMsg.newBuilder()
.setBranchName(branch),
pageLink
).build());
}
@Override
public ListenableFuture<PageData<EntityVersion>> listVersions(TenantId tenantId, String branch, EntityType entityType, PageLink pageLink) {
return listVersions(tenantId, ListVersionsRequestMsg.newBuilder()
.setBranchName(branch).setEntityType(entityType.name())
.setPageSize(pageLink.getPageSize())
.setPage(pageLink.getPage())
.build());
return listVersions(tenantId,
applyPageLinkParameters(
ListVersionsRequestMsg.newBuilder()
.setBranchName(branch)
.setEntityType(entityType.name()),
pageLink
).build());
}
@Override
public ListenableFuture<PageData<EntityVersion>> listVersions(TenantId tenantId, String branch, EntityId entityId, PageLink pageLink) {
return listVersions(tenantId, ListVersionsRequestMsg.newBuilder()
.setBranchName(branch)
.setEntityType(entityId.getEntityType().name())
.setEntityIdMSB(entityId.getId().getMostSignificantBits())
.setEntityIdLSB(entityId.getId().getLeastSignificantBits())
.setPageSize(pageLink.getPageSize())
.setPage(pageLink.getPage())
.build());
return listVersions(tenantId,
applyPageLinkParameters(
ListVersionsRequestMsg.newBuilder()
.setBranchName(branch)
.setEntityType(entityId.getEntityType().name())
.setEntityIdMSB(entityId.getId().getMostSignificantBits())
.setEntityIdLSB(entityId.getId().getLeastSignificantBits()),
pageLink
).build());
}
private ListVersionsRequestMsg.Builder applyPageLinkParameters(ListVersionsRequestMsg.Builder builder, PageLink pageLink) {
builder.setPageSize(pageLink.getPageSize())
.setPage(pageLink.getPage());
if (pageLink.getTextSearch() != null) {
builder.setTextSearch(pageLink.getTextSearch());
}
if (pageLink.getSortOrder() != null) {
if (pageLink.getSortOrder().getProperty() != null) {
builder.setSortProperty(pageLink.getSortOrder().getProperty());
}
if (pageLink.getSortOrder().getDirection() != null) {
builder.setSortDirection(pageLink.getSortOrder().getDirection().name());
}
}
return builder;
}
private ListenableFuture<PageData<EntityVersion>> listVersions(TenantId tenantId, ListVersionsRequestMsg requestMsg) {

6
application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultTbVersionControlSettingsService.java

@ -54,7 +54,7 @@ public class DefaultTbVersionControlSettingsService implements TbVersionControlS
@Override
public EntitiesVersionControlSettings get(TenantId tenantId) {
return cache.getAndPutInTransaction(tenantId, () -> {
EntitiesVersionControlSettings settings = cache.getAndPutInTransaction(tenantId, () -> {
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, SETTINGS_KEY);
if (adminSettings != null) {
try {
@ -65,6 +65,10 @@ public class DefaultTbVersionControlSettingsService implements TbVersionControlS
}
return null;
}, true);
if (settings != null) {
settings = new EntitiesVersionControlSettings(settings);
}
return settings;
}
@Override

6
application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java

@ -55,10 +55,10 @@ public interface EntitiesVersionControlService {
EntitiesVersionControlSettings getVersionControlSettings(TenantId tenantId);
EntitiesVersionControlSettings saveVersionControlSettings(TenantId tenantId, EntitiesVersionControlSettings versionControlSettings);
ListenableFuture<EntitiesVersionControlSettings> saveVersionControlSettings(TenantId tenantId, EntitiesVersionControlSettings versionControlSettings);
void deleteVersionControlSettings(TenantId tenantId) throws Exception;
ListenableFuture<Void> deleteVersionControlSettings(TenantId tenantId) throws Exception;
void checkVersionControlAccess(TenantId tenantId, EntitiesVersionControlSettings settings) throws Exception;
ListenableFuture<Void> checkVersionControlAccess(TenantId tenantId, EntitiesVersionControlSettings settings) throws Exception;
}

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

@ -931,6 +931,9 @@ queue:
tb_ota_package:
- key: max.poll.records
value: "${TB_QUEUE_KAFKA_OTA_MAX_POLL_RECORDS:10}"
tb_version_control:
- key: max.poll.interval.ms
value: "${TB_QUEUE_KAFKA_VC_MAX_POLL_INTERVAL_MS:600000}"
# tb_rule_engine.sq:
# - key: max.poll.records
# value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}"

3
common/cluster-api/src/main/proto/queue.proto

@ -724,6 +724,9 @@ message ListVersionsRequestMsg {
int64 entityIdLSB = 4;
int32 pageSize = 5;
int32 page = 6;
string textSearch = 7;
string sortProperty = 8;
string sortDirection = 9;
}
message EntityVersionProto {

13
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntitiesVersionControlSettings.java

@ -31,4 +31,17 @@ public class EntitiesVersionControlSettings implements Serializable {
private String privateKey;
private String privateKeyPassword;
private String defaultBranch;
public EntitiesVersionControlSettings() {}
public EntitiesVersionControlSettings(EntitiesVersionControlSettings settings) {
this.repositoryUri = settings.getRepositoryUri();
this.authMethod = settings.getAuthMethod();
this.username = settings.getUsername();
this.password = settings.getPassword();
this.privateKeyFileName = settings.getPrivateKeyFileName();
this.privateKey = settings.getPrivateKey();
this.privateKeyPassword = settings.getPrivateKeyPassword();
this.defaultBranch = settings.getDefaultBranch();
}
}

12
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java

@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.sync.vc.EntitiesVersionControlSettings;
import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo;
@ -295,7 +296,16 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
} else {
path = null;
}
var data = vcService.listVersions(ctx.getTenantId(), request.getBranchName(), path, new PageLink(request.getPageSize(), request.getPage()));
SortOrder sortOrder = null;
if (StringUtils.isNotEmpty(request.getSortProperty())) {
var direction = SortOrder.Direction.DESC;
if (StringUtils.isNotEmpty(request.getSortDirection())) {
direction = SortOrder.Direction.valueOf(request.getSortDirection());
}
sortOrder = new SortOrder(request.getSortProperty(), direction);
}
var data = vcService.listVersions(ctx.getTenantId(), request.getBranchName(), path,
new PageLink(request.getPageSize(), request.getPage(), request.getTextSearch(), sortOrder));
reply(ctx, Optional.empty(), builder ->
builder.setListVersionsResponse(ListVersionsResponseMsg.newBuilder()
.setTotalPages(data.getTotalPages())

48
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java

@ -42,6 +42,7 @@ import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.RefSpec;
@ -181,11 +182,18 @@ public class GitRepository {
return new PageData<>();
}
LogCommand command = git.log()
.add(branchId)
.setRevFilter(RevFilter.NO_MERGES);
.add(branchId);
if (StringUtils.isNotEmpty(pageLink.getTextSearch())) {
command.setRevFilter(new NoMergesAndCommitMessageFilter(pageLink.getTextSearch()));
} else {
command.setRevFilter(RevFilter.NO_MERGES);
}
if (StringUtils.isNotEmpty(path)) {
command.addPath(path);
}
Iterable<RevCommit> commits = execute(command);
return iterableToPageData(commits, this::toCommit, pageLink, revCommitComparatorFunction);
}
@ -241,7 +249,10 @@ public class GitRepository {
public Status status() throws GitAPIException {
org.eclipse.jgit.api.Status status = execute(git.status());
return new Status(status.getAdded(), status.getModified(), status.getRemoved());
Set<String> modified = new HashSet<>();
modified.addAll(status.getModified());
modified.addAll(status.getChanged());
return new Status(status.getAdded(), modified, status.getRemoved());
}
public Commit commit(String message) throws GitAPIException {
@ -314,7 +325,7 @@ public class GitRepository {
}
private Commit toCommit(RevCommit revCommit) {
return new Commit(revCommit.getCommitTime() * 1000L, revCommit.getName(), revCommit.getFullMessage(), revCommit.getAuthorIdent().getName());
return new Commit(revCommit.getCommitTime() * 1000l, revCommit.getName(), revCommit.getFullMessage(), revCommit.getAuthorIdent().getName());
}
private RevCommit resolveCommit(String id) throws IOException {
@ -424,6 +435,35 @@ public class GitRepository {
return keyPairs;
}
private static class NoMergesAndCommitMessageFilter extends RevFilter {
private final String textSearch;
NoMergesAndCommitMessageFilter(String textSearch) {
this.textSearch = textSearch.toLowerCase();
}
@Override
public boolean include(RevWalk walker, RevCommit c) {
return c.getParentCount() < 2 && c.getFullMessage().toLowerCase().contains(this.textSearch);
}
@Override
public RevFilter clone() {
return this;
}
@Override
public boolean requiresCommitBody() {
return false;
}
@Override
public String toString() {
return "NO_MERGES_AND_COMMIT_MESSAGE";
}
}
@Data
public static class Commit {
private final long timestamp;

3
msa/vc-executor/src/main/resources/tb-vc-executor.yml

@ -73,6 +73,9 @@ queue:
tb_ota_package:
- key: max.poll.records
value: "${TB_QUEUE_KAFKA_OTA_MAX_POLL_RECORDS:10}"
tb_version_control:
- key: max.poll.interval.ms
value: "${TB_QUEUE_KAFKA_VC_MAX_POLL_INTERVAL_MS:600000}"
# tb_rule_engine.sq:
# - key: max.poll.records
# value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}"

17
ui-ngx/src/app/core/http/admin.service.ts

@ -26,6 +26,8 @@ import {
TestSmsRequest,
UpdateMessage
} from '@shared/models/settings.models';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
@ -33,7 +35,8 @@ import {
export class AdminService {
constructor(
private http: HttpClient
private http: HttpClient,
private entitiesVersionControlService: EntitiesVersionControlService
) { }
public getAdminSettings<T>(key: string, config?: RequestConfig): Observable<AdminSettings<T>> {
@ -72,11 +75,19 @@ export class AdminService {
public saveEntitiesVersionControlSettings(versionControlSettings: EntitiesVersionControlSettings,
config?: RequestConfig): Observable<EntitiesVersionControlSettings> {
return this.http.post<EntitiesVersionControlSettings>('/api/admin/vcSettings', versionControlSettings,
defaultHttpOptionsFromConfig(config));
defaultHttpOptionsFromConfig(config)).pipe(
tap(() => {
this.entitiesVersionControlService.clearBranchList();
})
);
}
public deleteEntitiesVersionControlSettings(config?: RequestConfig) {
return this.http.delete('/api/admin/vcSettings', defaultHttpOptionsFromConfig(config));
return this.http.delete('/api/admin/vcSettings', defaultHttpOptionsFromConfig(config)).pipe(
tap(() => {
this.entitiesVersionControlService.clearBranchList();
})
);
}
public checkVersionControlAccess(versionControlSettings: EntitiesVersionControlSettings,

57
ui-ngx/src/app/core/http/entities-version-control.service.ts

@ -17,30 +17,69 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { Observable } from 'rxjs';
import { BranchInfo, EntityVersion, VersionCreateRequest, VersionCreationResult } from '@shared/models/vc.models';
import { combineLatest, Observable, of } from 'rxjs';
import {
BranchInfo,
EntityVersion,
VersionCreateRequest,
VersionCreationResult,
VersionLoadRequest, VersionLoadResult
} from '@shared/models/vc.models';
import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
import { DeviceInfo } from '@shared/models/device.models';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
import { createSelector, select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { selectHasVersionControl, selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors';
import { catchError, combineAll, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class EntitiesVersionControlService {
branchList: Array<BranchInfo> = null;
constructor(
private http: HttpClient
private http: HttpClient,
private store: Store<AppState>
) {
this.store.pipe(select(selectIsUserLoaded)).subscribe(
() => {
this.branchList = null;
}
);
}
public clearBranchList(): void {
this.branchList = null;
}
public listBranches(config?: RequestConfig): Observable<Array<BranchInfo>> {
return this.http.get<Array<BranchInfo>>('/api/entities/vc/branches', defaultHttpOptionsFromConfig(config));
public listBranches(): Observable<Array<BranchInfo>> {
if (!this.branchList) {
return this.http.get<Array<BranchInfo>>('/api/entities/vc/branches',
defaultHttpOptionsFromConfig({ignoreErrors: true, ignoreLoading: false})).pipe(
catchError(() => of([] as Array<BranchInfo>)),
tap((list) => {
this.branchList = list;
})
);
} else {
return of(this.branchList);
}
}
public saveEntitiesVersion(request: VersionCreateRequest, config?: RequestConfig): Observable<VersionCreationResult> {
return this.http.post<VersionCreationResult>('/api/entities/vc/version', request, defaultHttpOptionsFromConfig(config));
return this.http.post<VersionCreationResult>('/api/entities/vc/version', request, defaultHttpOptionsFromConfig(config)).pipe(
tap(() => {
const branch = request.branch;
if (this.branchList && !this.branchList.find(b => b.name === branch)) {
this.branchList = null;
}
})
);
}
public listEntityVersions(pageLink: PageLink, branch: string,
@ -62,4 +101,8 @@ export class EntitiesVersionControlService {
return this.http.get<PageData<EntityVersion>>(`/api/entities/vc/version/${branch}${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public loadEntitiesVersion(request: VersionLoadRequest, config?: RequestConfig): Observable<Array<VersionLoadResult>> {
return this.http.post<Array<VersionLoadResult>>('/api/entities/vc/entity', request, defaultHttpOptionsFromConfig(config));
}
}

1
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts

@ -285,6 +285,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
this.attributeScopeSelectionReadonly = true;
}
this.mode = 'default';
this.textSearchMode = false;
this.selectedWidgetsBundleAlias = null;
this.attributeScope = this.defaultAttributeScope;
this.pageLink.textSearch = null;

1
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -547,6 +547,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
}
resetSortAndFilter(update: boolean = true, preserveTimewindow: boolean = false) {
this.textSearchMode = false;
this.pageLink.textSearch = null;
if (this.entitiesTableConfig.useTimePageLink && !preserveTimewindow) {
this.timewindow = this.entitiesTableConfig.defaultTimewindowInterval;

13
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -153,10 +153,11 @@ import { TenantProfileQueuesComponent } from '@home/components/profile/queue/ten
import { QueueFormComponent } from '@home/components/queue/queue-form.component';
import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module';
import { WidgetSettingsComponent } from '@home/components/widget/widget-settings.component';
import { VcEntityExportDialogComponent } from '@home/components/vc/vc-entity-export-dialog.component';
import { VersionControlSettingsComponent } from '@home/components/vc/version-control-settings.component';
import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { EntityVersionsTableComponent } from '@home/components/vc/entity-versions-table.component';
import { EntityVersionExportComponent } from '@home/components/vc/entity-version-export.component';
import { EntityVersionRestoreComponent } from '@home/components/vc/entity-version-restore.component';
@NgModule({
declarations:
@ -281,10 +282,11 @@ import { EntityVersionsTableComponent } from '@home/components/vc/entity-version
DisplayWidgetTypesPanelComponent,
TenantProfileQueuesComponent,
QueueFormComponent,
VcEntityExportDialogComponent,
VersionControlSettingsComponent,
VersionControlComponent,
EntityVersionsTableComponent
EntityVersionsTableComponent,
EntityVersionExportComponent,
EntityVersionRestoreComponent
],
imports: [
CommonModule,
@ -403,10 +405,11 @@ import { EntityVersionsTableComponent } from '@home/components/vc/entity-version
DisplayWidgetTypesPanelComponent,
TenantProfileQueuesComponent,
QueueFormComponent,
VcEntityExportDialogComponent,
VersionControlSettingsComponent,
VersionControlComponent,
EntityVersionsTableComponent
EntityVersionsTableComponent,
EntityVersionExportComponent,
EntityVersionRestoreComponent
],
providers: [
WidgetComponentService,

6
ui-ngx/src/app/modules/home/components/profile/device-profile.component.html

@ -34,12 +34,6 @@
[fxShow]="!isEdit && !entity?.default">
{{'device-profile.set-default' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'vcExport')"
[fxShow]="!isEdit">
{{'version-control.export-to-git' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"

73
ui-ngx/src/app/modules/home/components/vc/entity-version-export.component.html

@ -0,0 +1,73 @@
<!--
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.
-->
<section style="min-width: 400px;">
<section *ngIf="!resultMessage">
<mat-toolbar>
<h2>{{ 'version-control.create-entity-version' | translate }}</h2>
<span fxFlex></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
*ngIf="isLoading$ | async">
</mat-progress-bar>
<form [formGroup]="exportFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<tb-branch-autocomplete
required
formControlName="branch">
</tb-branch-autocomplete>
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>version-control.version-name</mat-label>
<input required matInput formControlName="versionName">
<mat-error *ngIf="exportFormGroup.get('versionName').hasError('required')">
{{ 'version-control.version-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-checkbox formControlName="saveRelations" style="margin-bottom: 16px;">
{{ 'version-control.export-entity-relations' | translate }}
</mat-checkbox>
</div>
</fieldset>
</form>
<div fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="export()"
[disabled]="(isLoading$ | async) || exportFormGroup.invalid || !exportFormGroup.dirty">
{{ 'action.create' | translate }}
</button>
</div>
</section>
<section *ngIf="resultMessage">
<div class="mat-title export-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>

21
ui-ngx/src/app/modules/home/components/vc/entity-version-export.component.scss

@ -0,0 +1,21 @@
/**
* 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.
*/
:host {
.export-result-message {
padding: 48px 8px 8px;
text-align: center;
}
}

96
ui-ngx/src/app/modules/home/components/vc/entity-version-export.component.ts

@ -0,0 +1,96 @@
///
/// 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.
///
import { Component, Input, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import {
SingleEntityVersionCreateRequest,
VersionCreateRequestType,
VersionCreationResult
} from '@shared/models/vc.models';
import { Store } from '@ngrx/store';
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';
@Component({
selector: 'tb-entity-version-export',
templateUrl: './entity-version-export.component.html',
styleUrls: ['./entity-version-export.component.scss']
})
export class EntityVersionExportComponent extends PageComponent implements OnInit {
@Input()
branch: string;
@Input()
entityId: EntityId;
@Input()
onClose: (result: VersionCreationResult | null, branch: string | null) => void;
@Input()
onContentUpdated: () => void;
exportFormGroup: FormGroup;
resultMessage: string;
constructor(protected store: Store<AppState>,
private entitiesVersionControlService: EntitiesVersionControlService,
private translate: TranslateService,
private fb: FormBuilder) {
super(store);
}
ngOnInit(): void {
this.exportFormGroup = this.fb.group({
branch: [this.branch, [Validators.required]],
versionName: [null, [Validators.required]],
saveRelations: [false, []]
});
}
cancel(): void {
if (this.onClose) {
this.onClose(null, null);
}
}
export(): void {
const request: SingleEntityVersionCreateRequest = {
entityId: this.entityId,
branch: this.exportFormGroup.get('branch').value,
versionName: this.exportFormGroup.get('versionName').value,
config: {
saveRelations: this.exportFormGroup.get('saveRelations').value
},
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');
if (this.onContentUpdated) {
this.onContentUpdated();
}
} else if (this.onClose) {
this.onClose(result, request.branch);
}
});
}
}

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

@ -0,0 +1,49 @@
<!--
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.
-->
<section style="min-width: 400px;">
<mat-toolbar>
<h2>{{ 'version-control.restore-entity-from-version' | translate: {versionName} }}</h2>
<span fxFlex></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
*ngIf="isLoading$ | async">
</mat-progress-bar>
<form [formGroup]="restoreFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<mat-checkbox formControlName="loadRelations" style="margin-bottom: 16px;">
{{ 'version-control.load-entity-relations' | translate }}
</mat-checkbox>
</div>
</fieldset>
</form>
<div fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="restore()"
[disabled]="(isLoading$ | async) || restoreFormGroup.invalid">
{{ 'action.restore' | translate }}
</button>
</div>
</section>

86
ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts

@ -0,0 +1,86 @@
///
/// 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.
///
import { Component, Input, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { FormBuilder, FormGroup } from '@angular/forms';
import { SingleEntityVersionLoadRequest, VersionLoadRequestType, VersionLoadResult } from '@shared/models/vc.models';
import { Store } from '@ngrx/store';
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';
@Component({
selector: 'tb-entity-version-restore',
templateUrl: './entity-version-restore.component.html',
styleUrls: []
})
export class EntityVersionRestoreComponent extends PageComponent implements OnInit {
@Input()
branch: string;
@Input()
versionName: string;
@Input()
versionId: string;
@Input()
externalEntityId: EntityId;
@Input()
onClose: (result: Array<VersionLoadResult> | null) => void;
restoreFormGroup: FormGroup;
constructor(protected store: Store<AppState>,
private entitiesVersionControlService: EntitiesVersionControlService,
private translate: TranslateService,
private fb: FormBuilder) {
super(store);
}
ngOnInit(): void {
this.restoreFormGroup = this.fb.group({
loadRelations: [false, []]
});
}
cancel(): void {
if (this.onClose) {
this.onClose(null);
}
}
restore(): void {
const request: SingleEntityVersionLoadRequest = {
branch: this.branch,
versionId: this.versionId,
externalEntityId: this.externalEntityId,
config: {
loadRelations: this.restoreFormGroup.get('loadRelations').value
},
type: VersionLoadRequestType.SINGLE_ENTITY
};
this.entitiesVersionControlService.loadEntitiesVersion(request).subscribe((result) => {
if (this.onClose) {
this.onClose(result);
}
});
}
}

76
ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html

@ -17,7 +17,7 @@
-->
<div class="mat-padding tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content">
<mat-toolbar class="mat-table-toolbar">
<mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
<span class="tb-entity-table-title">{{(singleEntityMode ? 'version-control.entity-versions' : 'version-control.versions') | translate}}</span>
@ -31,10 +31,39 @@
</tb-branch-autocomplete>
</div>
<span fxFlex></span>
<button *ngIf="singleEntityMode" mat-raised-button color="primary"
<button *ngIf="singleEntityMode" mat-stroked-button color="primary"
#exportButton
[disabled]="(isLoading$ | async)"
(click)="vcExport($event)">
{{'version-control.export-to-git' | translate }}
(click)="toggleVcExport($event, exportButton)">
<mat-icon>update</mat-icon>
{{'version-control.create-version' | translate }}
</button>
<button mat-icon-button
[disabled]="isLoading$ | async"
(click)="enterFilterMode()"
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
</div>
</mat-toolbar>
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode">
<div class="mat-toolbar-tools">
<button mat-icon-button
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
<mat-form-field fxFlex>
<mat-label>&nbsp;</mat-label>
<input #searchInput matInput
[(ngModel)]="pageLink.textSearch"
placeholder="{{ 'common.enter-search' | translate }}"/>
</mat-form-field>
<button mat-icon-button (click)="exitFilterMode()"
matTooltip="{{ 'action.close' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</mat-toolbar>
@ -48,27 +77,56 @@
</mat-cell>
</ng-container>
<ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef style="width: 40%"> {{ 'version-control.version-id' | translate }} </mat-header-cell>
<mat-header-cell *matHeaderCellDef style="min-width: 100px; max-width: 100px; width: 100px;"> {{ 'version-control.version-id' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let entityVersion">
{{ entityVersion.id }}
<span style="display: inline-block; width: 54px;" [innerHTML]="versionIdContent(entityVersion)"></span>
<tb-copy-button
[disabled]="isLoading$ | async"
[copyText]="entityVersion.id"
tooltipText="{{ 'version-control.copy-full-version-id' | translate }}"
tooltipPosition="above"
icon="content_paste"
[style]="{
'font-size': '16px',
color: 'rgba(0,0,0,.87)'
}">
</tb-copy-button>
</mat-cell>
</ng-container>
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef style="width: 60%"> {{ 'version-control.version-name' | translate }} </mat-header-cell>
<mat-header-cell *matHeaderCellDef style="width: 100%"> {{ 'version-control.version-name' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let entityVersion">
{{ entityVersion.name }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef style="min-width: 40px; max-width: 40px; width: 40px">
</mat-header-cell>
<mat-cell *matCellDef="let entityVersion">
<div fxFlex fxLayout="row" fxLayoutAlign="end">
<button *ngIf="singleEntityMode" mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'version-control.restore-version' | translate }}"
matTooltipPosition="above"
#restoreVersionButton
(click)="toggleRestoreEntityVersion($event, restoreVersionButton, entityVersion)">
<mat-icon>restore</mat-icon>
</button>
</div>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row *matRowDef="let entityVersion; columns: displayedColumns;"></mat-row>
<mat-row [fxShow]="!dataSource.dataLoading" *matRowDef="let entityVersion; columns: displayedColumns;"></mat-row>
</table>
<span [fxShow]="dataSource.isEmpty() | async"
<span [fxShow]="!(isLoading$ | async) && (dataSource.isEmpty() | async) && !dataSource.dataLoading"
fxLayoutAlign="center center"
class="no-data-found" translate>{{
singleEntityMode
? 'version-control.no-entity-versions-text'
: 'version-control.no-versions-text'
}}</span>
<span [fxShow]="dataSource.dataLoading"
fxLayoutAlign="center center"
class="no-data-found">{{ 'common.loading' | translate }}</span>
</div>
<mat-divider></mat-divider>
<mat-paginator [length]="dataSource.total() | async"

2
ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.scss

@ -78,7 +78,7 @@
tb-branch-autocomplete {
mat-form-field {
font-size: 16px;
width: 200px;
width: 250px;
.mat-form-field-wrapper {
padding-bottom: 0;

152
ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.ts

@ -18,22 +18,22 @@ import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
ElementRef, EventEmitter,
Input,
OnDestroy,
OnInit,
ViewChild
OnInit, Output, Renderer2,
ViewChild, ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityId, entityIdEquals } from '@shared/models/id/entity-id';
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { BehaviorSubject, merge, Observable, of, ReplaySubject } from 'rxjs';
import { BehaviorSubject, fromEvent, merge, Observable, of, ReplaySubject } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { PageLink } from '@shared/models/page/page-link';
import { catchError, map, tap } from 'rxjs/operators';
import { EntityVersion } from '@shared/models/vc.models';
import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators';
import { EntityVersion, VersionCreationResult, VersionLoadResult } from '@shared/models/vc.models';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
@ -41,6 +41,12 @@ import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { BranchAutocompleteComponent } from '@shared/components/vc/branch-autocomplete.component';
import { isNotEmptyStr } from '@core/utils';
import { TbPopoverService } from '@shared/components/popover.service';
import { EntityVersionExportComponent } from '@home/components/vc/entity-version-export.component';
import { MatButton } from '@angular/material/button';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { EntityVersionRestoreComponent } from '@home/components/vc/entity-version-restore.component';
@Component({
selector: 'tb-entity-versions-table',
@ -54,8 +60,9 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
@Input()
singleEntityMode = false;
displayedColumns = ['timestamp', 'id', 'name'];
displayedColumns = ['timestamp', 'id', 'name', 'actions'];
pageLink: PageLink;
textSearchMode = false;
dataSource: EntityVersionsDatasource;
hidePageSize = false;
@ -84,7 +91,7 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
@Input()
set externalEntityId(externalEntityId: EntityId) {
if (this.externalEntityIdValue !== externalEntityId) {
if (!entityIdEquals(this.externalEntityIdValue, externalEntityId)) {
this.externalEntityIdValue = externalEntityId;
this.resetSortAndFilter(this.activeValue);
if (!this.activeValue) {
@ -93,12 +100,23 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
}
}
@Input()
entityId: EntityId;
@Output()
versionRestored = new EventEmitter<void>();
@ViewChild('searchInput') searchInputField: ElementRef;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
constructor(protected store: Store<AppState>,
private entitiesVersionControlService: EntitiesVersionControlService,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private cd: ChangeDetectorRef,
private viewContainerRef: ViewContainerRef,
private elementRef: ElementRef) {
super(store);
this.dirtyValue = !this.activeValue;
@ -125,14 +143,27 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
}
branchChanged(newBranch: string) {
this.branch = newBranch;
this.paginator.pageIndex = 0;
if (this.activeValue) {
this.updateData();
if (isNotEmptyStr(newBranch) && this.branch !== newBranch) {
this.branch = newBranch;
this.paginator.pageIndex = 0;
if (this.activeValue) {
this.updateData();
}
}
}
ngAfterViewInit() {
fromEvent(this.searchInputField.nativeElement, 'keyup')
.pipe(
debounceTime(400),
distinctUntilChanged(),
tap(() => {
this.paginator.pageIndex = 0;
this.updateData();
})
)
.subscribe();
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
merge(this.sort.sortChange, this.paginator.page)
.pipe(
@ -140,19 +171,102 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
)
.subscribe();
this.viewsInited = true;
if (!this.singleEntityMode) {
if (!this.singleEntityMode || (this.activeValue && this.externalEntityIdValue)) {
this.initFromDefaultBranch();
}
}
vcExport($event: Event) {
toggleVcExport($event: Event, exportButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = exportButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const vcExportPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, EntityVersionExportComponent, 'left', true, null,
{
branch: this.branch,
entityId: this.entityId,
onClose: (result: VersionCreationResult | null, branch: string | null) => {
vcExportPopover.hide();
if (result) {
if (this.branch !== branch) {
this.branchChanged(branch);
} else {
this.updateData();
}
}
},
onContentUpdated: () => {
vcExportPopover.updatePosition();
setTimeout(() => {
vcExportPopover.updatePosition();
});
}
}, {}, {}, {}, false);
}
}
toggleRestoreEntityVersion($event: Event, restoreVersionButton: MatButton, entityVersion: EntityVersion) {
if ($event) {
$event.stopPropagation();
}
const trigger = restoreVersionButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const restoreVersionPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, EntityVersionRestoreComponent, 'left', true, null,
{
branch: this.branch,
versionName: entityVersion.name,
versionId: entityVersion.id,
externalEntityId: this.externalEntityIdValue,
onClose: (result: Array<VersionLoadResult> | null) => {
restoreVersionPopover.hide();
if (result && result.length) {
this.versionRestored.emit();
}
}
}, {}, {}, {}, false);
}
}
versionIdContent(entityVersion: EntityVersion): string {
let versionId = entityVersion.id;
if (versionId.length > 7) {
versionId = versionId.slice(0, 7);
}
return versionId;
}
enterFilterMode() {
this.textSearchMode = true;
this.pageLink.textSearch = '';
setTimeout(() => {
this.searchInputField.nativeElement.focus();
this.searchInputField.nativeElement.setSelectionRange(0, 0);
}, 10);
}
exitFilterMode() {
this.textSearchMode = false;
this.pageLink.textSearch = null;
this.paginator.pageIndex = 0;
this.updateData();
}
private initFromDefaultBranch() {
this.branchAutocompleteComponent.selectDefaultBranchIfNeeded(false, true);
if (this.branchAutocompleteComponent.isDefaultBranchSelected()) {
this.paginator.pageIndex = 0;
if (this.activeValue) {
this.updateData();
}
} else {
this.branchAutocompleteComponent.selectDefaultBranchIfNeeded(true);
}
}
private updateData() {
@ -164,7 +278,7 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
}
private resetSortAndFilter(update: boolean) {
this.branch = null;
this.textSearchMode = false;
this.pageLink.textSearch = null;
if (this.viewsInited) {
this.paginator.pageIndex = 0;
@ -185,6 +299,8 @@ class EntityVersionsDatasource implements DataSource<EntityVersion> {
public pageData$ = this.pageDataSubject.asObservable();
public dataLoading = true;
constructor(private entitiesVersionControlService: EntitiesVersionControlService) {}
connect(collectionViewer: CollectionViewer): Observable<EntityVersion[] | ReadonlyArray<EntityVersion>> {
@ -199,6 +315,7 @@ class EntityVersionsDatasource implements DataSource<EntityVersion> {
loadEntityVersions(singleEntityMode: boolean,
branch: string, externalEntityId: EntityId,
pageLink: PageLink): Observable<PageData<EntityVersion>> {
this.dataLoading = true;
const result = new ReplaySubject<PageData<EntityVersion>>();
this.fetchEntityVersions(singleEntityMode, branch, externalEntityId, pageLink).pipe(
catchError(() => of(emptyPageData<EntityVersion>())),
@ -207,6 +324,7 @@ class EntityVersionsDatasource implements DataSource<EntityVersion> {
this.entityVersionsSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
result.next(pageData);
this.dataLoading = false;
}
);
return result;

77
ui-ngx/src/app/modules/home/components/vc/vc-entity-export-dialog.component.html

@ -1,77 +0,0 @@
<!--
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.
-->
<section style="min-width: 400px;">
<mat-toolbar color="primary">
<h2>{{ (createResult ? 'version-control.entity-version-exported' : 'version-control.export-entity-version') | translate }}</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<form *ngIf="!createResult" [formGroup]="exportFormGroup">
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<tb-branch-autocomplete
required
formControlName="branch">
</tb-branch-autocomplete>
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>version-control.version-name</mat-label>
<input required matInput formControlName="versionName">
<mat-error *ngIf="exportFormGroup.get('versionName').hasError('required')">
{{ 'version-control.version-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-checkbox formControlName="saveRelations" style="margin-bottom: 16px;">
{{ 'version-control.export-entity-relations' | translate }}
</mat-checkbox>
</div>
</fieldset>
</form>
<div *ngIf="createResult" fxFlex fxLayout="column" fxLayoutAlign="center">
<div [innerHTML]="createResultMessage"></div>
</div>
</div>
<div *ngIf="!createResult" mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="export()"
[disabled]="(isLoading$ | async) || exportFormGroup.invalid || !exportFormGroup.dirty">
{{ 'action.export' | translate }}
</button>
</div>
<div *ngIf="createResult" mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
</section>

105
ui-ngx/src/app/modules/home/components/vc/vc-entity-export-dialog.component.ts

@ -1,105 +0,0 @@
///
/// 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.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { EntityId } from '@shared/models/id/entity-id';
import {
SingleEntityVersionCreateRequest,
VersionCreateRequestType,
VersionCreationResult
} from '@shared/models/vc.models';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
export interface VcEntityExportDialogData {
entityId: EntityId;
}
@Component({
selector: 'tb-vc-entity-export-dialog',
templateUrl: './vc-entity-export-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: VcEntityExportDialogComponent}],
styleUrls: []
})
export class VcEntityExportDialogComponent extends DialogComponent<VcEntityExportDialogComponent>
implements OnInit, ErrorStateMatcher {
exportFormGroup: FormGroup;
submitted = false;
createResult: VersionCreationResult;
createResultMessage: SafeHtml;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: VcEntityExportDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<VcEntityExportDialogComponent>,
private entitiesVersionControlService: EntitiesVersionControlService,
private translate: TranslateService,
private domSanitizer: DomSanitizer,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.exportFormGroup = this.fb.group({
branch: [null, [Validators.required]],
versionName: [null, [Validators.required]],
saveRelations: [false, []]
});
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close();
}
export(): void {
this.submitted = true;
const request: SingleEntityVersionCreateRequest = {
entityId: this.data.entityId,
branch: this.exportFormGroup.get('branch').value,
versionName: this.exportFormGroup.get('versionName').value,
config: {
saveRelations: this.exportFormGroup.get('saveRelations').value
},
type: VersionCreateRequestType.SINGLE_ENTITY
};
this.entitiesVersionControlService.saveEntitiesVersion(request).subscribe((result) => {
this.createResult = result;
const message = this.translate.instant('version-control.export-entity-version-result-message',
{name: result.version.name, commitId: result.version.id});
this.createResultMessage = this.domSanitizer.bypassSecurityTrustHtml(message);
});
}
}

3
ui-ngx/src/app/modules/home/components/vc/version-control-settings.component.ts

@ -33,7 +33,7 @@ import { DialogService } from '@core/services/dialog.service';
import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions';
import { ActionAuthUpdateHasVersionControl } from '@core/auth/auth.actions';
import { selectHasVersionControl } from '@core/auth/auth.selectors';
import { catchError, mergeMap } from 'rxjs/operators';
import { catchError, mergeMap, take } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({
@ -87,6 +87,7 @@ export class VersionControlSettingsComponent extends PageComponent implements On
});
this.store.pipe(
select(selectHasVersionControl),
take(1),
mergeMap((hasVersionControl) => {
if (hasVersionControl) {
return this.adminService.getEntitiesVersionControlSettings({ignoreErrors: true}).pipe(

4
ui-ngx/src/app/modules/home/components/vc/version-control.component.html

@ -21,5 +21,7 @@
<ng-template #versionsTable>
<tb-entity-versions-table [singleEntityMode]="singleEntityMode"
[active]="active"
[externalEntityId]="externalEntityId"></tb-entity-versions-table>
[entityId]="entityId"
[externalEntityId]="externalEntityId"
(versionRestored)="versionRestored.emit()"></tb-entity-versions-table>
</ng-template>

8
ui-ngx/src/app/modules/home/components/vc/version-control.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { selectHasVersionControl } from '@core/auth/auth.selectors';
@ -44,6 +44,12 @@ export class VersionControlComponent implements OnInit, HasConfirmForm {
@Input()
externalEntityId: EntityId;
@Input()
entityId: EntityId;
@Output()
versionRestored = new EventEmitter<void>();
hasVersionControl$ = this.store.pipe(select(selectHasVersionControl));
constructor(private store: Store<AppState>) {

16
ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts

@ -22,11 +22,6 @@ import {
ImportDialogCsvComponent,
ImportDialogCsvData
} from '@home/components/import-export/import-dialog-csv.component';
import { EntityId } from '@shared/models/id/entity-id';
import {
VcEntityExportDialogComponent,
VcEntityExportDialogData
} from '@home/components/vc/vc-entity-export-dialog.component';
@Injectable()
export class HomeDialogsService {
@ -46,17 +41,6 @@ export class HomeDialogsService {
}
}
public exportVcEntity(entityId: EntityId): Observable<void> {
return this.dialog.open<VcEntityExportDialogComponent, VcEntityExportDialogData>(VcEntityExportDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
entityId
}
}).afterClosed();
}
private openImportDialogCSV(entityType: EntityType, importTitle: string, importFileLabel: string): Observable<boolean> {
return this.dialog.open<ImportDialogCsvComponent, ImportDialogCsvData,
any>(ImportDialogCsvComponent, {

6
ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html

@ -49,3 +49,9 @@
label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

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

@ -46,12 +46,6 @@
[fxShow]="!isEdit && assetScope === 'edge'">
{{ 'edge.unassign-from-edge' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'vcExport')"
[fxShow]="!isEdit && (assetScope === 'customer' || assetScope === 'tenant')">
{{'version-control.export-to-git' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"

10
ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts

@ -468,13 +468,6 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
);
}
vcExport($event: Event, asset: Asset) {
if ($event) {
$event.stopPropagation();
}
this.homeDialogs.exportVcEntity(asset.id);
}
onAssetAction(action: EntityAction<AssetInfo>, config: EntityTableConfig<AssetInfo>): boolean {
switch (action.action) {
case 'open':
@ -492,9 +485,6 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
case 'unassignFromEdge':
this.unassignFromEdge(action.event, action.entity);
return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
}
return false;
}

6
ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.html

@ -49,3 +49,9 @@
label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.CUSTOMER" [customerId]="entity.id"></tb-audit-log-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

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

@ -53,12 +53,6 @@
*ngIf="edgesSupportEnabled()">
{{'customer.manage-edges' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'vcExport')"
[fxShow]="!isEdit && !isPublic">
{{'version-control.export-to-git' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"

10
ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts

@ -182,13 +182,6 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
this.router.navigateByUrl(`customers/${customer.id.id}/edgeInstances`);
}
vcExport($event: Event, customer: Customer) {
if ($event) {
$event.stopPropagation();
}
this.homeDialogs.exportVcEntity(customer.id);
}
onCustomerAction(action: EntityAction<Customer>, config: EntityTableConfig<Customer>): boolean {
switch (action.action) {
case 'open':
@ -209,9 +202,6 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
case 'manageEdges':
this.manageCustomerEdges(action.event, action.entity);
return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
}
return false;
}

6
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html

@ -59,12 +59,6 @@
[fxShow]="!isEdit && dashboardScope === 'edge'">
{{ 'edge.unassign-from-edge' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'vcExport')"
[fxShow]="!isEdit && dashboardScope === 'tenant'">
{{'version-control.export-to-git' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"

6
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-tabs.component.html

@ -19,3 +19,9 @@
label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id" detailsMode="true"></tb-audit-log-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

10
ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts

@ -539,13 +539,6 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
);
}
vcExport($event: Event, dashboard: DashboardInfo) {
if ($event) {
$event.stopPropagation();
}
this.homeDialogs.exportVcEntity(dashboard.id);
}
onDashboardAction(action: EntityAction<DashboardInfo>): boolean {
switch (action.action) {
case 'open':
@ -569,9 +562,6 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
case 'unassignFromEdge':
this.unassignFromEdge(action.event, action.entity);
return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
}
return false;
}

6
ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html

@ -75,3 +75,9 @@
label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN && !isEdit"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

10
ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts

@ -206,13 +206,6 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
this.importExport.exportDeviceProfile(deviceProfile.id.id);
}
vcExport($event: Event, deviceProfile: DeviceProfile) {
if ($event) {
$event.stopPropagation();
}
this.homeDialogs.exportVcEntity(deviceProfile.id);
}
onDeviceProfileAction(action: EntityAction<DeviceProfile>): boolean {
switch (action.action) {
case 'open':
@ -224,9 +217,6 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
case 'export':
this.exportDeviceProfile(action.event, action.entity);
return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
}
return false;
}

3
ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html

@ -52,5 +52,6 @@
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
[active]="versionControlTab.isActive" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

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

@ -52,12 +52,6 @@
[fxShow]="!isEdit && deviceScope === 'edge'">
{{ 'edge.unassign-from-edge' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'vcExport')"
[fxShow]="!isEdit && (deviceScope === 'customer' || deviceScope === 'tenant')">
{{'version-control.export-to-git' | translate }}
</button>
<button mat-raised-button color="primary" fxFlex.xs
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"

10
ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts

@ -547,13 +547,6 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
});
}
vcExport($event: Event, device: Device) {
if ($event) {
$event.stopPropagation();
}
this.homeDialogs.exportVcEntity(device.id);
}
onDeviceAction(action: EntityAction<DeviceInfo>, config: EntityTableConfig<DeviceInfo>): boolean {
switch (action.action) {
case 'open':
@ -574,9 +567,6 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
case 'manageCredentials':
this.manageCredentials(action.event, action.entity);
return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
}
return false;
}

6
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-tabs.component.html

@ -52,3 +52,9 @@
label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

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

@ -58,12 +58,6 @@
[fxShow]="!isEdit && !isEdgeRootRuleChain() && ruleChainScope === 'edge'">
{{'edge.unassign-from-edge' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'vcExport')"
[fxShow]="!isEdit">
{{'version-control.export-to-git' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"

10
ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts

@ -360,13 +360,6 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
);
}
vcExport($event: Event, ruleChain: RuleChain) {
if ($event) {
$event.stopPropagation();
}
this.homeDialogs.exportVcEntity(ruleChain.id);
}
onRuleChainAction(action: EntityAction<RuleChain>): boolean {
switch (action.action) {
case 'open':
@ -390,9 +383,6 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
case 'unsetAutoAssignToEdge':
this.unsetAutoAssignToEdgeRuleChain(action.event, action.entity);
return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
}
return false;
}

3
ui-ngx/src/app/shared/components/popover.component.ts

@ -314,7 +314,7 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
<span class="tb-popover-arrow-content"></span>
</div>
<div class="tb-popover-inner" [ngStyle]="tbPopoverInnerStyle" role="tooltip">
<div class="tb-popover-close-button" (click)="closeButtonClick($event)">×</div>
<div *ngIf="tbShowCloseButton" class="tb-popover-close-button" (click)="closeButtonClick($event)">×</div>
<div style="width: 100%; height: 100%;">
<div class="tb-popover-inner-content">
<ng-container *ngIf="tbContent">
@ -354,6 +354,7 @@ export class TbPopoverComponent implements OnDestroy, OnInit {
tbMouseEnterDelay?: number;
tbMouseLeaveDelay?: number;
tbHideOnClickOutside = true;
tbShowCloseButton = true;
tbAnimationState = 'active';

4
ui-ngx/src/app/shared/components/popover.service.ts

@ -55,7 +55,8 @@ export class TbPopoverService {
displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true,
injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any): TbPopoverComponent {
injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any,
showCloseButton = true): TbPopoverComponent {
const componentRef = hostView.createComponent(this.componentFactory);
const component = componentRef.instance;
this.popoverWithTriggers.push({
@ -76,6 +77,7 @@ export class TbPopoverService {
component.tbPopoverInnerStyle = popoverStyle;
component.tbComponentStyle = style;
component.tbHideOnClickOutside = hideOnClickOutside;
component.tbShowCloseButton = showCloseButton;
component.tbVisibleChange.subscribe((visible: boolean) => {
if (!visible) {
component.tbAnimationDone.subscribe(() => {

13
ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html

@ -17,12 +17,14 @@
-->
<mat-form-field [formGroup]="branchFormGroup" class="mat-block" [floatLabel]="selectionMode ? 'always' : 'auto'">
<mat-label>{{ 'version-control.branch' | translate }}</mat-label>
<input matInput type="text" placeholder="{{ 'version-control.select-branch' | translate }}"
<input matInput type="text" placeholder="{{(loading ? 'common.loading' : 'version-control.select-branch') | translate}}"
#branchInput
formControlName="branch"
(keydown.enter)="branchInput.blur(); autoCompleteTrigger.closePanel();"
(focusin)="onFocus()"
(blur)="onBlur()"
[required]="required"
[matAutocomplete]="subTypeAutocomplete">
[matAutocomplete]="branchAutocomplete">
<button *ngIf="branchFormGroup.get('branch').value && !disabled"
type="button"
matSuffix mat-button mat-icon-button aria-label="Clear"
@ -32,10 +34,13 @@
<mat-autocomplete
class="tb-autocomplete"
(closed)="onPanelClosed()"
#subTypeAutocomplete="matAutocomplete"
#branchAutocomplete="matAutocomplete"
[displayWith]="displayBranchFn">
<mat-option *ngFor="let branch of filteredBranches | async" [value]="branch">
<mat-option *ngFor="let branch of filteredBranches | async" [value]="branch" class="branch-option">
<mat-icon class="tb-mat-18" *ngIf="selectionMode && branch.name === modelValue">check</mat-icon>
<span class="check-placeholder" *ngIf="selectionMode && branch.name !== modelValue"></span>
<span [innerHTML]="branch.name | highlight:searchText"></span>
<small *ngIf="branch.default" class="default-branch">{{ 'version-control.default' | translate }}</small>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="branchFormGroup.get('branch').hasError('required')">

30
ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.scss

@ -0,0 +1,30 @@
/**
* 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.
*/
.mat-option.branch-option {
.mat-icon, .check-placeholder {
margin-right: 8px;
}
.check-placeholder {
width: 18px;
display: inline-block;
}
.mat-option-text {
width: 100%;
.default-branch {
float: right;
}
}
}

129
ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts

@ -16,44 +16,36 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
Input,
NgZone,
OnInit,
ViewChild
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of } from 'rxjs';
import {
catchError,
debounceTime,
distinctUntilChanged,
map,
publishReplay,
refCount,
share,
switchMap,
tap
} from 'rxjs/operators';
import { Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { BranchInfo } from '@shared/models/vc.models';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { isNotEmptyStr } from '@core/utils';
import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete';
@Component({
selector: 'tb-branch-autocomplete',
templateUrl: './branch-autocomplete.component.html',
styleUrls: [],
styleUrls: ['./branch-autocomplete.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BranchAutocompleteComponent),
multi: true
}]
}],
encapsulation: ViewEncapsulation.None
})
export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit {
@ -94,19 +86,21 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
@Input()
selectionMode = false;
@ViewChild('branchAutocomplete') matAutocomplete: MatAutocomplete;
@ViewChild('branchInput', { read: MatAutocompleteTrigger, static: true }) autoCompleteTrigger: MatAutocompleteTrigger;
@ViewChild('branchInput', {static: true}) branchInput: ElementRef;
filteredBranches: Observable<Array<BranchInfo>>;
branches: Observable<Array<BranchInfo>> = null;
defaultBranch: BranchInfo = null;
searchText = '';
loading = false;
private dirty = false;
private ignoreClosedPanel = false;
private clearButtonClicked = false;
private propagateChange = (v: any) => { };
@ -140,7 +134,9 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
} else {
modelValue = value;
}
this.updateView(modelValue);
if (!this.selectionMode || modelValue) {
this.updateView(modelValue);
}
}),
map(value => {
if (value) {
@ -167,17 +163,33 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
this.disabled = isDisabled;
}
selectDefaultBranchIfNeeded(ignoreLoading = true, force = false): void {
isDefaultBranchSelected(): boolean {
return this.defaultBranch && this.defaultBranch.name === this.modelValue;
}
selectDefaultBranchIfNeeded(force = false): void {
if ((this.selectDefaultBranch && !this.modelValue) || force) {
this.getBranches(ignoreLoading).subscribe(
(data) => {
if (this.defaultBranch || force) {
this.branchFormGroup.get('branch').patchValue(this.defaultBranch, {emitEvent: false});
this.modelValue = this.defaultBranch?.name;
this.propagateChange(this.modelValue);
}
setTimeout(() => {
if (this.defaultBranch) {
this.branchFormGroup.get('branch').patchValue(this.defaultBranch, {emitEvent: false});
this.modelValue = this.defaultBranch?.name;
this.propagateChange(this.modelValue);
} else {
this.loading = true;
this.getBranches().subscribe(
() => {
if (this.defaultBranch || force) {
this.branchFormGroup.get('branch').patchValue(this.defaultBranch, {emitEvent: false});
this.modelValue = this.defaultBranch?.name;
this.propagateChange(this.modelValue);
this.loading = false;
} else {
this.loading = false;
}
}
);
}
);
});
}
}
@ -200,15 +212,37 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
}
}
onBlur() {
if (this.clearButtonClicked) {
this.clearButtonClicked = false;
} else if (!this.matAutocomplete.isOpen) {
this.selectAvailableValue();
}
}
onPanelClosed() {
if (this.ignoreClosedPanel) {
this.ignoreClosedPanel = false;
} else {
if (this.selectionMode && !this.branchFormGroup.get('branch').value && this.defaultBranch) {
this.selectAvailableValue();
}
selectAvailableValue() {
if (this.selectionMode) {
const branch = this.branchFormGroup.get('branch').value;
this.getBranches().pipe(
map(branches => {
let foundBranch = branches.find(b => b.name === branch);
if (!foundBranch && isNotEmptyStr(this.modelValue)) {
foundBranch = branches.find(b => b.name === this.modelValue);
}
return foundBranch;
})
).subscribe((val) => {
if (!val && this.defaultBranch) {
val = this.defaultBranch;
}
this.zone.run(() => {
this.branchFormGroup.get('branch').patchValue(this.defaultBranch, {emitEvent: true});
this.branchFormGroup.get('branch').patchValue(val, {emitEvent: true});
}, 0);
}
});
}
}
@ -223,7 +257,7 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
return branch ? branch.name : undefined;
}
fetchBranches(searchText?: string): Observable<Array<BranchInfo>> {
private fetchBranches(searchText?: string): Observable<Array<BranchInfo>> {
this.searchText = searchText;
return this.getBranches().pipe(
map(branches => {
@ -239,25 +273,18 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
);
}
getBranches(ignoreLoading = true): Observable<Array<BranchInfo>> {
if (!this.branches) {
const branchesObservable = this.entitiesVersionControlService.listBranches({ignoreLoading, ignoreErrors: true});
this.branches = branchesObservable.pipe(
catchError(() => of([] as Array<BranchInfo>)),
tap((data) => {
this.defaultBranch = data.find(branch => branch.default);
}),
publishReplay(1),
refCount()
);
}
return this.branches;
private getBranches(): Observable<Array<BranchInfo>> {
return this.entitiesVersionControlService.listBranches().pipe(
tap((data) => {
this.defaultBranch = data.find(branch => branch.default);
})
);
}
clear() {
this.ignoreClosedPanel = true;
this.branchFormGroup.get('branch').patchValue(null, {emitEvent: true});
this.clearButtonClicked = true;
setTimeout(() => {
this.branchFormGroup.get('branch').patchValue(null, {emitEvent: true});
this.branchInput.nativeElement.blur();
this.branchInput.nativeElement.focus();
}, 0);

29
ui-ngx/src/app/shared/models/vc.models.ts

@ -15,16 +15,26 @@
///
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
export interface VersionCreateConfig {
saveRelations: boolean;
}
export interface VersionLoadConfig {
loadRelations: boolean;
}
export enum VersionCreateRequestType {
SINGLE_ENTITY = 'SINGLE_ENTITY',
COMPLEX = 'COMPLEX'
}
export enum VersionLoadRequestType {
SINGLE_ENTITY = 'SINGLE_ENTITY',
ENTITY_TYPE = 'ENTITY_TYPE'
}
export interface VersionCreateRequest {
versionName: string;
branch: string;
@ -37,6 +47,18 @@ export interface SingleEntityVersionCreateRequest extends VersionCreateRequest {
type: VersionCreateRequestType.SINGLE_ENTITY;
}
export interface VersionLoadRequest {
branch: string;
versionId: string;
type: VersionLoadRequestType;
}
export interface SingleEntityVersionLoadRequest extends VersionLoadRequest {
externalEntityId: EntityId;
config: VersionLoadConfig;
type: VersionLoadRequestType.SINGLE_ENTITY;
}
export interface BranchInfo {
name: string;
default: boolean;
@ -54,3 +76,10 @@ export interface VersionCreationResult {
modified: number;
removed: number;
}
export interface VersionLoadResult {
entityType: EntityType;
created: number;
updated: number;
deleted: number;
}

17
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -57,7 +57,8 @@
"download": "Download",
"next-with-label": "Next: {{label}}",
"read-more": "Read more",
"hide": "Hide"
"hide": "Hide",
"restore": "Restore"
},
"aggregation": {
"aggregation": "Aggregation",
@ -3112,21 +3113,25 @@
"version-control": "Version control",
"management": "Version control management",
"branch": "Branch",
"default": "Default",
"select-branch": "Select branch",
"branch-required": "Branch is required",
"export-entity-version": "Export entity version",
"entity-version-exported": "Entity version successfully exported",
"create-entity-version": "Create entity version",
"version-name": "Version name",
"version-name-required": "Version name is required",
"export-entity-relations": "Export entity relations",
"export-entity-version-result-message": "Entity exported with version '{{name}}' and commit id '{{commitId}}'.",
"export-to-git": "Export to Git",
"entity-versions": "Entity versions",
"versions": "Versions",
"created-time": "Created time",
"version-id": "Version ID",
"no-entity-versions-text": "No entity versions found",
"no-versions-text": "No versions found"
"no-versions-text": "No versions found",
"copy-full-version-id": "Copy full version id",
"create-version": "Create version",
"nothing-to-commit": "No changes to commit",
"restore-version": "Restore version",
"restore-entity-from-version": "Restore entity from version '{{versionName}}'",
"load-entity-relations": "Load entity relations"
},
"widget": {
"widget-library": "Widgets Library",

Loading…
Cancel
Save