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; package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.node.ObjectNode; 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.ApiOperation;
import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; 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.MailService;
import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.AdminSettings; 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) notes = "Creates or Updates the version control settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/vcSettings") @PostMapping("/vcSettings")
public EntitiesVersionControlSettings saveVersionControlSettings(@RequestBody EntitiesVersionControlSettings settings) throws ThingsboardException { public DeferredResult<EntitiesVersionControlSettings> saveVersionControlSettings(@RequestBody EntitiesVersionControlSettings settings) throws ThingsboardException {
try { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); ListenableFuture<EntitiesVersionControlSettings> future = versionControlService.saveVersionControlSettings(getTenantId(), settings);
EntitiesVersionControlSettings versionControlSettings = checkNotNull(versionControlService.saveVersionControlSettings(getTenantId(), settings)); return wrapFuture(Futures.transform(future, savedSettings -> {
versionControlSettings.setPassword(null); savedSettings.setPassword(null);
versionControlSettings.setPrivateKey(null); savedSettings.setPrivateKey(null);
versionControlSettings.setPrivateKeyPassword(null); savedSettings.setPrivateKeyPassword(null);
return versionControlSettings; return savedSettings;
} catch (Exception e) { }, MoreExecutors.directExecutor()));
throw handleException(e);
}
} }
@ApiOperation(value = "Delete version control settings (deleteVersionControlSettings)", @ApiOperation(value = "Delete version control settings (deleteVersionControlSettings)",
@ -240,10 +242,10 @@ public class AdminController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/vcSettings", method = RequestMethod.DELETE) @RequestMapping(value = "/vcSettings", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void deleteVersionControlSettings() throws ThingsboardException { public DeferredResult<Void> deleteVersionControlSettings() throws ThingsboardException {
try { try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.DELETE); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.DELETE);
versionControlService.deleteVersionControlSettings(getTenantId()); return wrapFuture(versionControlService.deleteVersionControlSettings(getTenantId()));
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }
@ -253,13 +255,13 @@ public class AdminController extends BaseController {
notes = "Attempts to check version control access. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Attempts to check version control access. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/vcSettings/checkAccess", method = RequestMethod.POST) @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.") @ApiParam(value = "A JSON value representing the Entities Version Control Settings.")
@RequestBody EntitiesVersionControlSettings settings) throws ThingsboardException { @RequestBody EntitiesVersionControlSettings settings) throws ThingsboardException {
try { try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
settings = checkNotNull(settings); settings = checkNotNull(settings);
versionControlService.checkVersionControlAccess(getTenantId(), settings); return wrapFuture(versionControlService.checkVersionControlAccess(getTenantId(), settings));
} catch (Exception e) { } catch (Exception e) {
throw handleException(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_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 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_START = "```json\n";
protected static final String MARKDOWN_CODE_BLOCK_END = "\n```"; protected static final String MARKDOWN_CODE_BLOCK_END = "\n```";
protected static final String EVENT_ERROR_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START + 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.List;
import java.util.UUID; 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; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
@RestController @RestController
@ -132,13 +137,15 @@ public class EntitiesVersionControlController extends BaseController {
@RequestParam int pageSize, @RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page, @RequestParam int page,
@ApiParam(value = ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp") @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp")
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException { @RequestParam(required = false) String sortOrder) throws ThingsboardException {
try { try {
EntityId externalEntityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid); 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)); return wrapFuture(versionControlService.listEntityVersions(getTenantId(), branch, externalEntityId, pageLink));
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
@ -159,12 +166,14 @@ public class EntitiesVersionControlController extends BaseController {
@RequestParam int pageSize, @RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page, @RequestParam int page,
@ApiParam(value = ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp") @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp")
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException { @RequestParam(required = false) String sortOrder) throws ThingsboardException {
try { 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)); return wrapFuture(versionControlService.listEntityTypeVersions(getTenantId(), branch, entityType, pageLink));
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
@ -192,12 +201,14 @@ public class EntitiesVersionControlController extends BaseController {
@RequestParam int pageSize, @RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page, @RequestParam int page,
@ApiParam(value = ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp") @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp")
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException { @RequestParam(required = false) String sortOrder) throws ThingsboardException {
try { try {
PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return wrapFuture(versionControlService.listVersions(getTenantId(), branch, pageLink)); return wrapFuture(versionControlService.listVersions(getTenantId(), branch, pageLink));
} catch (Exception e) { } catch (Exception e) {
throw handleException(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; SingleEntityVersionLoadRequest versionLoadRequest = (SingleEntityVersionLoadRequest) request;
VersionLoadConfig config = versionLoadRequest.getConfig(); VersionLoadConfig config = versionLoadRequest.getConfig();
ListenableFuture<EntityExportData> future = gitServiceQueue.getEntity(user.getTenantId(), request.getVersionId(), versionLoadRequest.getExternalEntityId()); 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 -> { EntityImportResult<?> importResult = transactionTemplate.execute(status -> {
try { try {
return exportImportService.importEntity(user, entityData, EntityImportSettings.builder() return exportImportService.importEntity(user, entityData, EntityImportSettings.builder()
@ -336,31 +336,30 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
} }
@Override @Override
public EntitiesVersionControlSettings saveVersionControlSettings(TenantId tenantId, EntitiesVersionControlSettings versionControlSettings) { public ListenableFuture<EntitiesVersionControlSettings> saveVersionControlSettings(TenantId tenantId, EntitiesVersionControlSettings versionControlSettings) {
versionControlSettings = this.vcSettingsService.restore(tenantId, versionControlSettings); var restoredSettings = this.vcSettingsService.restore(tenantId, versionControlSettings);
try { try {
//TODO: ashvayka: replace future.get with deferred result. Don't forget to call when tenant is deleted. var future = gitServiceQueue.initRepository(tenantId, restoredSettings);
gitServiceQueue.initRepository(tenantId, versionControlSettings).get(); return Futures.transform(future, f -> vcSettingsService.save(tenantId, restoredSettings), MoreExecutors.directExecutor());
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Failed to init repository!", e); throw new RuntimeException("Failed to init repository!", e);
} }
return vcSettingsService.save(tenantId, versionControlSettings);
} }
@Override @Override
public void deleteVersionControlSettings(TenantId tenantId) throws Exception { public ListenableFuture<Void> deleteVersionControlSettings(TenantId tenantId) throws Exception {
if (vcSettingsService.delete(tenantId)) { if (vcSettingsService.delete(tenantId)) {
//TODO: ashvayka: replace future.get with deferred result. Don't forget to call when tenant is deleted. return gitServiceQueue.clearRepository(tenantId);
gitServiceQueue.clearRepository(tenantId).get(); } else {
return Futures.immediateFuture(null);
} }
} }
@Override @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); settings = this.vcSettingsService.restore(tenantId, settings);
try { try {
//TODO: ashvayka: replace future.get with deferred result. return gitServiceQueue.testRepository(tenantId, settings);
gitServiceQueue.testRepository(tenantId, settings).get();
} catch (Exception e) { } catch (Exception e) {
throw new ThingsboardException(String.format("Unable to access repository: %s", getCauseMessage(e)), throw new ThingsboardException(String.format("Unable to access repository: %s", getCauseMessage(e)),
ThingsboardErrorCode.GENERAL); 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 @Override
public ListenableFuture<PageData<EntityVersion>> listVersions(TenantId tenantId, String branch, PageLink pageLink) { public ListenableFuture<PageData<EntityVersion>> listVersions(TenantId tenantId, String branch, PageLink pageLink) {
return listVersions(tenantId, ListVersionsRequestMsg.newBuilder()
.setBranchName(branch) return listVersions(tenantId,
.setPageSize(pageLink.getPageSize()) applyPageLinkParameters(
.setPage(pageLink.getPage()) ListVersionsRequestMsg.newBuilder()
.build()); .setBranchName(branch),
pageLink
).build());
} }
@Override @Override
public ListenableFuture<PageData<EntityVersion>> listVersions(TenantId tenantId, String branch, EntityType entityType, PageLink pageLink) { public ListenableFuture<PageData<EntityVersion>> listVersions(TenantId tenantId, String branch, EntityType entityType, PageLink pageLink) {
return listVersions(tenantId, ListVersionsRequestMsg.newBuilder() return listVersions(tenantId,
.setBranchName(branch).setEntityType(entityType.name()) applyPageLinkParameters(
.setPageSize(pageLink.getPageSize()) ListVersionsRequestMsg.newBuilder()
.setPage(pageLink.getPage()) .setBranchName(branch)
.build()); .setEntityType(entityType.name()),
pageLink
).build());
} }
@Override @Override
public ListenableFuture<PageData<EntityVersion>> listVersions(TenantId tenantId, String branch, EntityId entityId, PageLink pageLink) { public ListenableFuture<PageData<EntityVersion>> listVersions(TenantId tenantId, String branch, EntityId entityId, PageLink pageLink) {
return listVersions(tenantId, ListVersionsRequestMsg.newBuilder() return listVersions(tenantId,
.setBranchName(branch) applyPageLinkParameters(
.setEntityType(entityId.getEntityType().name()) ListVersionsRequestMsg.newBuilder()
.setEntityIdMSB(entityId.getId().getMostSignificantBits()) .setBranchName(branch)
.setEntityIdLSB(entityId.getId().getLeastSignificantBits()) .setEntityType(entityId.getEntityType().name())
.setPageSize(pageLink.getPageSize()) .setEntityIdMSB(entityId.getId().getMostSignificantBits())
.setPage(pageLink.getPage()) .setEntityIdLSB(entityId.getId().getLeastSignificantBits()),
.build()); 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) { 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 @Override
public EntitiesVersionControlSettings get(TenantId tenantId) { public EntitiesVersionControlSettings get(TenantId tenantId) {
return cache.getAndPutInTransaction(tenantId, () -> { EntitiesVersionControlSettings settings = cache.getAndPutInTransaction(tenantId, () -> {
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, SETTINGS_KEY); AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, SETTINGS_KEY);
if (adminSettings != null) { if (adminSettings != null) {
try { try {
@ -65,6 +65,10 @@ public class DefaultTbVersionControlSettingsService implements TbVersionControlS
} }
return null; return null;
}, true); }, true);
if (settings != null) {
settings = new EntitiesVersionControlSettings(settings);
}
return settings;
} }
@Override @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 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: tb_ota_package:
- key: max.poll.records - key: max.poll.records
value: "${TB_QUEUE_KAFKA_OTA_MAX_POLL_RECORDS:10}" 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: # tb_rule_engine.sq:
# - key: max.poll.records # - key: max.poll.records
# value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" # 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; int64 entityIdLSB = 4;
int32 pageSize = 5; int32 pageSize = 5;
int32 page = 6; int32 page = 6;
string textSearch = 7;
string sortProperty = 8;
string sortDirection = 9;
} }
message EntityVersionProto { 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 privateKey;
private String privateKeyPassword; private String privateKeyPassword;
private String defaultBranch; 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.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageLink; 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.EntitiesVersionControlSettings;
import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo; import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo;
@ -295,7 +296,16 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
} else { } else {
path = null; 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 -> reply(ctx, Optional.empty(), builder ->
builder.setListVersionsResponse(ListVersionsResponseMsg.newBuilder() builder.setListVersionsResponse(ListVersionsResponseMsg.newBuilder()
.setTotalPages(data.getTotalPages()) .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.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.filter.RevFilter; import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RefSpec;
@ -181,11 +182,18 @@ public class GitRepository {
return new PageData<>(); return new PageData<>();
} }
LogCommand command = git.log() LogCommand command = git.log()
.add(branchId) .add(branchId);
.setRevFilter(RevFilter.NO_MERGES);
if (StringUtils.isNotEmpty(pageLink.getTextSearch())) {
command.setRevFilter(new NoMergesAndCommitMessageFilter(pageLink.getTextSearch()));
} else {
command.setRevFilter(RevFilter.NO_MERGES);
}
if (StringUtils.isNotEmpty(path)) { if (StringUtils.isNotEmpty(path)) {
command.addPath(path); command.addPath(path);
} }
Iterable<RevCommit> commits = execute(command); Iterable<RevCommit> commits = execute(command);
return iterableToPageData(commits, this::toCommit, pageLink, revCommitComparatorFunction); return iterableToPageData(commits, this::toCommit, pageLink, revCommitComparatorFunction);
} }
@ -241,7 +249,10 @@ public class GitRepository {
public Status status() throws GitAPIException { public Status status() throws GitAPIException {
org.eclipse.jgit.api.Status status = execute(git.status()); 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 { public Commit commit(String message) throws GitAPIException {
@ -314,7 +325,7 @@ public class GitRepository {
} }
private Commit toCommit(RevCommit revCommit) { 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 { private RevCommit resolveCommit(String id) throws IOException {
@ -424,6 +435,35 @@ public class GitRepository {
return keyPairs; 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 @Data
public static class Commit { public static class Commit {
private final long timestamp; private final long timestamp;

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

@ -73,6 +73,9 @@ queue:
tb_ota_package: tb_ota_package:
- key: max.poll.records - key: max.poll.records
value: "${TB_QUEUE_KAFKA_OTA_MAX_POLL_RECORDS:10}" 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: # tb_rule_engine.sq:
# - key: max.poll.records # - key: max.poll.records
# value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" # 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, TestSmsRequest,
UpdateMessage UpdateMessage
} from '@shared/models/settings.models'; } from '@shared/models/settings.models';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { tap } from 'rxjs/operators';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -33,7 +35,8 @@ import {
export class AdminService { export class AdminService {
constructor( constructor(
private http: HttpClient private http: HttpClient,
private entitiesVersionControlService: EntitiesVersionControlService
) { } ) { }
public getAdminSettings<T>(key: string, config?: RequestConfig): Observable<AdminSettings<T>> { public getAdminSettings<T>(key: string, config?: RequestConfig): Observable<AdminSettings<T>> {
@ -72,11 +75,19 @@ export class AdminService {
public saveEntitiesVersionControlSettings(versionControlSettings: EntitiesVersionControlSettings, public saveEntitiesVersionControlSettings(versionControlSettings: EntitiesVersionControlSettings,
config?: RequestConfig): Observable<EntitiesVersionControlSettings> { config?: RequestConfig): Observable<EntitiesVersionControlSettings> {
return this.http.post<EntitiesVersionControlSettings>('/api/admin/vcSettings', versionControlSettings, return this.http.post<EntitiesVersionControlSettings>('/api/admin/vcSettings', versionControlSettings,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config)).pipe(
tap(() => {
this.entitiesVersionControlService.clearBranchList();
})
);
} }
public deleteEntitiesVersionControlSettings(config?: RequestConfig) { 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, 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 { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { Observable } from 'rxjs'; import { combineLatest, Observable, of } from 'rxjs';
import { BranchInfo, EntityVersion, VersionCreateRequest, VersionCreationResult } from '@shared/models/vc.models'; import {
BranchInfo,
EntityVersion,
VersionCreateRequest,
VersionCreationResult,
VersionLoadRequest, VersionLoadResult
} from '@shared/models/vc.models';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { DeviceInfo } from '@shared/models/device.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models'; 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({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class EntitiesVersionControlService { export class EntitiesVersionControlService {
branchList: Array<BranchInfo> = null;
constructor( 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>> { public listBranches(): Observable<Array<BranchInfo>> {
return this.http.get<Array<BranchInfo>>('/api/entities/vc/branches', defaultHttpOptionsFromConfig(config)); 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> { 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, 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()}`, return this.http.get<PageData<EntityVersion>>(`/api/entities/vc/version/${branch}${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config)); 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.attributeScopeSelectionReadonly = true;
} }
this.mode = 'default'; this.mode = 'default';
this.textSearchMode = false;
this.selectedWidgetsBundleAlias = null; this.selectedWidgetsBundleAlias = null;
this.attributeScope = this.defaultAttributeScope; this.attributeScope = this.defaultAttributeScope;
this.pageLink.textSearch = null; 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) { resetSortAndFilter(update: boolean = true, preserveTimewindow: boolean = false) {
this.textSearchMode = false;
this.pageLink.textSearch = null; this.pageLink.textSearch = null;
if (this.entitiesTableConfig.useTimePageLink && !preserveTimewindow) { if (this.entitiesTableConfig.useTimePageLink && !preserveTimewindow) {
this.timewindow = this.entitiesTableConfig.defaultTimewindowInterval; 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 { QueueFormComponent } from '@home/components/queue/queue-form.component';
import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module'; import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module';
import { WidgetSettingsComponent } from '@home/components/widget/widget-settings.component'; 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 { VersionControlSettingsComponent } from '@home/components/vc/version-control-settings.component';
import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { EntityVersionsTableComponent } from '@home/components/vc/entity-versions-table.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({ @NgModule({
declarations: declarations:
@ -281,10 +282,11 @@ import { EntityVersionsTableComponent } from '@home/components/vc/entity-version
DisplayWidgetTypesPanelComponent, DisplayWidgetTypesPanelComponent,
TenantProfileQueuesComponent, TenantProfileQueuesComponent,
QueueFormComponent, QueueFormComponent,
VcEntityExportDialogComponent,
VersionControlSettingsComponent, VersionControlSettingsComponent,
VersionControlComponent, VersionControlComponent,
EntityVersionsTableComponent EntityVersionsTableComponent,
EntityVersionExportComponent,
EntityVersionRestoreComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -403,10 +405,11 @@ import { EntityVersionsTableComponent } from '@home/components/vc/entity-version
DisplayWidgetTypesPanelComponent, DisplayWidgetTypesPanelComponent,
TenantProfileQueuesComponent, TenantProfileQueuesComponent,
QueueFormComponent, QueueFormComponent,
VcEntityExportDialogComponent,
VersionControlSettingsComponent, VersionControlSettingsComponent,
VersionControlComponent, VersionControlComponent,
EntityVersionsTableComponent EntityVersionsTableComponent,
EntityVersionExportComponent,
EntityVersionRestoreComponent
], ],
providers: [ providers: [
WidgetComponentService, WidgetComponentService,

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

@ -34,12 +34,6 @@
[fxShow]="!isEdit && !entity?.default"> [fxShow]="!isEdit && !entity?.default">
{{'device-profile.set-default' | translate }} {{'device-profile.set-default' | translate }}
</button> </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" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')" (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 class="mat-padding tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content"> <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 class="mat-toolbar-tools">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container"> <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> <span class="tb-entity-table-title">{{(singleEntityMode ? 'version-control.entity-versions' : 'version-control.versions') | translate}}</span>
@ -31,10 +31,39 @@
</tb-branch-autocomplete> </tb-branch-autocomplete>
</div> </div>
<span fxFlex></span> <span fxFlex></span>
<button *ngIf="singleEntityMode" mat-raised-button color="primary" <button *ngIf="singleEntityMode" mat-stroked-button color="primary"
#exportButton
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="vcExport($event)"> (click)="toggleVcExport($event, exportButton)">
{{'version-control.export-to-git' | translate }} <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> </button>
</div> </div>
</mat-toolbar> </mat-toolbar>
@ -48,27 +77,56 @@
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="id"> <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"> <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> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="name"> <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"> <mat-cell *matCellDef="let entityVersion">
{{ entityVersion.name }} {{ entityVersion.name }}
</mat-cell> </mat-cell>
</ng-container> </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-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> </table>
<span [fxShow]="dataSource.isEmpty() | async" <span [fxShow]="!(isLoading$ | async) && (dataSource.isEmpty() | async) && !dataSource.dataLoading"
fxLayoutAlign="center center" fxLayoutAlign="center center"
class="no-data-found" translate>{{ class="no-data-found" translate>{{
singleEntityMode singleEntityMode
? 'version-control.no-entity-versions-text' ? 'version-control.no-entity-versions-text'
: 'version-control.no-versions-text' : 'version-control.no-versions-text'
}}</span> }}</span>
<span [fxShow]="dataSource.dataLoading"
fxLayoutAlign="center center"
class="no-data-found">{{ 'common.loading' | translate }}</span>
</div> </div>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<mat-paginator [length]="dataSource.total() | async" <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 { tb-branch-autocomplete {
mat-form-field { mat-form-field {
font-size: 16px; font-size: 16px;
width: 200px; width: 250px;
.mat-form-field-wrapper { .mat-form-field-wrapper {
padding-bottom: 0; padding-bottom: 0;

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

@ -18,22 +18,22 @@ import {
AfterViewInit, AfterViewInit,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef, EventEmitter,
Input, Input,
OnDestroy, OnDestroy,
OnInit, OnInit, Output, Renderer2,
ViewChild ViewChild, ViewContainerRef
} from '@angular/core'; } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; 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 { 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 { emptyPageData, PageData } from '@shared/models/page/page-data';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { catchError, map, tap } from 'rxjs/operators'; import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators';
import { EntityVersion } from '@shared/models/vc.models'; import { EntityVersion, VersionCreationResult, VersionLoadResult } from '@shared/models/vc.models';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service'; import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
@ -41,6 +41,12 @@ import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants'; import { hidePageSizePixelValue } from '@shared/models/constants';
import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { BranchAutocompleteComponent } from '@shared/components/vc/branch-autocomplete.component'; 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({ @Component({
selector: 'tb-entity-versions-table', selector: 'tb-entity-versions-table',
@ -54,8 +60,9 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
@Input() @Input()
singleEntityMode = false; singleEntityMode = false;
displayedColumns = ['timestamp', 'id', 'name']; displayedColumns = ['timestamp', 'id', 'name', 'actions'];
pageLink: PageLink; pageLink: PageLink;
textSearchMode = false;
dataSource: EntityVersionsDatasource; dataSource: EntityVersionsDatasource;
hidePageSize = false; hidePageSize = false;
@ -84,7 +91,7 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
@Input() @Input()
set externalEntityId(externalEntityId: EntityId) { set externalEntityId(externalEntityId: EntityId) {
if (this.externalEntityIdValue !== externalEntityId) { if (!entityIdEquals(this.externalEntityIdValue, externalEntityId)) {
this.externalEntityIdValue = externalEntityId; this.externalEntityIdValue = externalEntityId;
this.resetSortAndFilter(this.activeValue); this.resetSortAndFilter(this.activeValue);
if (!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(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private entitiesVersionControlService: EntitiesVersionControlService, private entitiesVersionControlService: EntitiesVersionControlService,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private viewContainerRef: ViewContainerRef,
private elementRef: ElementRef) { private elementRef: ElementRef) {
super(store); super(store);
this.dirtyValue = !this.activeValue; this.dirtyValue = !this.activeValue;
@ -125,14 +143,27 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
} }
branchChanged(newBranch: string) { branchChanged(newBranch: string) {
this.branch = newBranch; if (isNotEmptyStr(newBranch) && this.branch !== newBranch) {
this.paginator.pageIndex = 0; this.branch = newBranch;
if (this.activeValue) { this.paginator.pageIndex = 0;
this.updateData(); if (this.activeValue) {
this.updateData();
}
} }
} }
ngAfterViewInit() { 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); this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
merge(this.sort.sortChange, this.paginator.page) merge(this.sort.sortChange, this.paginator.page)
.pipe( .pipe(
@ -140,19 +171,102 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
) )
.subscribe(); .subscribe();
this.viewsInited = true; this.viewsInited = true;
if (!this.singleEntityMode) { if (!this.singleEntityMode || (this.activeValue && this.externalEntityIdValue)) {
this.initFromDefaultBranch(); 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) { if ($event) {
$event.stopPropagation(); $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() { 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() { private updateData() {
@ -164,7 +278,7 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
} }
private resetSortAndFilter(update: boolean) { private resetSortAndFilter(update: boolean) {
this.branch = null; this.textSearchMode = false;
this.pageLink.textSearch = null; this.pageLink.textSearch = null;
if (this.viewsInited) { if (this.viewsInited) {
this.paginator.pageIndex = 0; this.paginator.pageIndex = 0;
@ -185,6 +299,8 @@ class EntityVersionsDatasource implements DataSource<EntityVersion> {
public pageData$ = this.pageDataSubject.asObservable(); public pageData$ = this.pageDataSubject.asObservable();
public dataLoading = true;
constructor(private entitiesVersionControlService: EntitiesVersionControlService) {} constructor(private entitiesVersionControlService: EntitiesVersionControlService) {}
connect(collectionViewer: CollectionViewer): Observable<EntityVersion[] | ReadonlyArray<EntityVersion>> { connect(collectionViewer: CollectionViewer): Observable<EntityVersion[] | ReadonlyArray<EntityVersion>> {
@ -199,6 +315,7 @@ class EntityVersionsDatasource implements DataSource<EntityVersion> {
loadEntityVersions(singleEntityMode: boolean, loadEntityVersions(singleEntityMode: boolean,
branch: string, externalEntityId: EntityId, branch: string, externalEntityId: EntityId,
pageLink: PageLink): Observable<PageData<EntityVersion>> { pageLink: PageLink): Observable<PageData<EntityVersion>> {
this.dataLoading = true;
const result = new ReplaySubject<PageData<EntityVersion>>(); const result = new ReplaySubject<PageData<EntityVersion>>();
this.fetchEntityVersions(singleEntityMode, branch, externalEntityId, pageLink).pipe( this.fetchEntityVersions(singleEntityMode, branch, externalEntityId, pageLink).pipe(
catchError(() => of(emptyPageData<EntityVersion>())), catchError(() => of(emptyPageData<EntityVersion>())),
@ -207,6 +324,7 @@ class EntityVersionsDatasource implements DataSource<EntityVersion> {
this.entityVersionsSubject.next(pageData.data); this.entityVersionsSubject.next(pageData.data);
this.pageDataSubject.next(pageData); this.pageDataSubject.next(pageData);
result.next(pageData); result.next(pageData);
this.dataLoading = false;
} }
); );
return result; 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 { ActionSettingsChangeLanguage } from '@core/settings/settings.actions';
import { ActionAuthUpdateHasVersionControl } from '@core/auth/auth.actions'; import { ActionAuthUpdateHasVersionControl } from '@core/auth/auth.actions';
import { selectHasVersionControl } from '@core/auth/auth.selectors'; import { selectHasVersionControl } from '@core/auth/auth.selectors';
import { catchError, mergeMap } from 'rxjs/operators'; import { catchError, mergeMap, take } from 'rxjs/operators';
import { of } from 'rxjs'; import { of } from 'rxjs';
@Component({ @Component({
@ -87,6 +87,7 @@ export class VersionControlSettingsComponent extends PageComponent implements On
}); });
this.store.pipe( this.store.pipe(
select(selectHasVersionControl), select(selectHasVersionControl),
take(1),
mergeMap((hasVersionControl) => { mergeMap((hasVersionControl) => {
if (hasVersionControl) { if (hasVersionControl) {
return this.adminService.getEntitiesVersionControlSettings({ignoreErrors: true}).pipe( 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> <ng-template #versionsTable>
<tb-entity-versions-table [singleEntityMode]="singleEntityMode" <tb-entity-versions-table [singleEntityMode]="singleEntityMode"
[active]="active" [active]="active"
[externalEntityId]="externalEntityId"></tb-entity-versions-table> [entityId]="entityId"
[externalEntityId]="externalEntityId"
(versionRestored)="versionRestored.emit()"></tb-entity-versions-table>
</ng-template> </ng-template>

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

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

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

@ -22,11 +22,6 @@ import {
ImportDialogCsvComponent, ImportDialogCsvComponent,
ImportDialogCsvData ImportDialogCsvData
} from '@home/components/import-export/import-dialog-csv.component'; } 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() @Injectable()
export class HomeDialogsService { 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> { private openImportDialogCSV(entityType: EntityType, importTitle: string, importFileLabel: string): Observable<boolean> {
return this.dialog.open<ImportDialogCsvComponent, ImportDialogCsvData, return this.dialog.open<ImportDialogCsvComponent, ImportDialogCsvData,
any>(ImportDialogCsvComponent, { 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"> 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> <tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab> </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'"> [fxShow]="!isEdit && assetScope === 'edge'">
{{ 'edge.unassign-from-edge' | translate }} {{ 'edge.unassign-from-edge' | translate }}
</button> </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" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')" (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 { onAssetAction(action: EntityAction<AssetInfo>, config: EntityTableConfig<AssetInfo>): boolean {
switch (action.action) { switch (action.action) {
case 'open': case 'open':
@ -492,9 +485,6 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
case 'unassignFromEdge': case 'unassignFromEdge':
this.unassignFromEdge(action.event, action.entity); this.unassignFromEdge(action.event, action.entity);
return true; return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
} }
return false; 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"> 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> <tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.CUSTOMER" [customerId]="entity.id"></tb-audit-log-table>
</mat-tab> </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()"> *ngIf="edgesSupportEnabled()">
{{'customer.manage-edges' | translate }} {{'customer.manage-edges' | translate }}
</button> </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" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')" (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`); 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 { onCustomerAction(action: EntityAction<Customer>, config: EntityTableConfig<Customer>): boolean {
switch (action.action) { switch (action.action) {
case 'open': case 'open':
@ -209,9 +202,6 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
case 'manageEdges': case 'manageEdges':
this.manageCustomerEdges(action.event, action.entity); this.manageCustomerEdges(action.event, action.entity);
return true; return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
} }
return false; return false;
} }

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

@ -59,12 +59,6 @@
[fxShow]="!isEdit && dashboardScope === 'edge'"> [fxShow]="!isEdit && dashboardScope === 'edge'">
{{ 'edge.unassign-from-edge' | translate }} {{ 'edge.unassign-from-edge' | translate }}
</button> </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" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')" (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"> 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> <tb-audit-log-table [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id" detailsMode="true"></tb-audit-log-table>
</mat-tab> </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 { onDashboardAction(action: EntityAction<DashboardInfo>): boolean {
switch (action.action) { switch (action.action) {
case 'open': case 'open':
@ -569,9 +562,6 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
case 'unassignFromEdge': case 'unassignFromEdge':
this.unassignFromEdge(action.event, action.entity); this.unassignFromEdge(action.event, action.entity);
return true; return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
} }
return false; 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 }}"> 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> <tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab> </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); 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 { onDeviceProfileAction(action: EntityAction<DeviceProfile>): boolean {
switch (action.action) { switch (action.action) {
case 'open': case 'open':
@ -224,9 +217,6 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
case 'export': case 'export':
this.exportDeviceProfile(action.event, action.entity); this.exportDeviceProfile(action.event, action.entity);
return true; return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
} }
return false; 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" <mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab"> label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true" <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> </mat-tab>

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

@ -52,12 +52,6 @@
[fxShow]="!isEdit && deviceScope === 'edge'"> [fxShow]="!isEdit && deviceScope === 'edge'">
{{ 'edge.unassign-from-edge' | translate }} {{ 'edge.unassign-from-edge' | translate }}
</button> </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 <button mat-raised-button color="primary" fxFlex.xs
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')" (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 { onDeviceAction(action: EntityAction<DeviceInfo>, config: EntityTableConfig<DeviceInfo>): boolean {
switch (action.action) { switch (action.action) {
case 'open': case 'open':
@ -574,9 +567,6 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
case 'manageCredentials': case 'manageCredentials':
this.manageCredentials(action.event, action.entity); this.manageCredentials(action.event, action.entity);
return true; return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
} }
return false; 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"> 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> <tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab> </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'"> [fxShow]="!isEdit && !isEdgeRootRuleChain() && ruleChainScope === 'edge'">
{{'edge.unassign-from-edge' | translate }} {{'edge.unassign-from-edge' | translate }}
</button> </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" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')" (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 { onRuleChainAction(action: EntityAction<RuleChain>): boolean {
switch (action.action) { switch (action.action) {
case 'open': case 'open':
@ -390,9 +383,6 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
case 'unsetAutoAssignToEdge': case 'unsetAutoAssignToEdge':
this.unsetAutoAssignToEdgeRuleChain(action.event, action.entity); this.unsetAutoAssignToEdgeRuleChain(action.event, action.entity);
return true; return true;
case 'vcExport':
this.vcExport(action.event, action.entity);
return true;
} }
return false; 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> <span class="tb-popover-arrow-content"></span>
</div> </div>
<div class="tb-popover-inner" [ngStyle]="tbPopoverInnerStyle" role="tooltip"> <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 style="width: 100%; height: 100%;">
<div class="tb-popover-inner-content"> <div class="tb-popover-inner-content">
<ng-container *ngIf="tbContent"> <ng-container *ngIf="tbContent">
@ -354,6 +354,7 @@ export class TbPopoverComponent implements OnDestroy, OnInit {
tbMouseEnterDelay?: number; tbMouseEnterDelay?: number;
tbMouseLeaveDelay?: number; tbMouseLeaveDelay?: number;
tbHideOnClickOutside = true; tbHideOnClickOutside = true;
tbShowCloseButton = true;
tbAnimationState = 'active'; 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, displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true, 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 componentRef = hostView.createComponent(this.componentFactory);
const component = componentRef.instance; const component = componentRef.instance;
this.popoverWithTriggers.push({ this.popoverWithTriggers.push({
@ -76,6 +77,7 @@ export class TbPopoverService {
component.tbPopoverInnerStyle = popoverStyle; component.tbPopoverInnerStyle = popoverStyle;
component.tbComponentStyle = style; component.tbComponentStyle = style;
component.tbHideOnClickOutside = hideOnClickOutside; component.tbHideOnClickOutside = hideOnClickOutside;
component.tbShowCloseButton = showCloseButton;
component.tbVisibleChange.subscribe((visible: boolean) => { component.tbVisibleChange.subscribe((visible: boolean) => {
if (!visible) { if (!visible) {
component.tbAnimationDone.subscribe(() => { 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-form-field [formGroup]="branchFormGroup" class="mat-block" [floatLabel]="selectionMode ? 'always' : 'auto'">
<mat-label>{{ 'version-control.branch' | translate }}</mat-label> <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 #branchInput
formControlName="branch" formControlName="branch"
(keydown.enter)="branchInput.blur(); autoCompleteTrigger.closePanel();"
(focusin)="onFocus()" (focusin)="onFocus()"
(blur)="onBlur()"
[required]="required" [required]="required"
[matAutocomplete]="subTypeAutocomplete"> [matAutocomplete]="branchAutocomplete">
<button *ngIf="branchFormGroup.get('branch').value && !disabled" <button *ngIf="branchFormGroup.get('branch').value && !disabled"
type="button" type="button"
matSuffix mat-button mat-icon-button aria-label="Clear" matSuffix mat-button mat-icon-button aria-label="Clear"
@ -32,10 +34,13 @@
<mat-autocomplete <mat-autocomplete
class="tb-autocomplete" class="tb-autocomplete"
(closed)="onPanelClosed()" (closed)="onPanelClosed()"
#subTypeAutocomplete="matAutocomplete" #branchAutocomplete="matAutocomplete"
[displayWith]="displayBranchFn"> [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> <span [innerHTML]="branch.name | highlight:searchText"></span>
<small *ngIf="branch.default" class="default-branch">{{ 'version-control.default' | translate }}</small>
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
<mat-error *ngIf="branchFormGroup.get('branch').hasError('required')"> <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 { import {
AfterViewInit, AfterViewInit,
ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
forwardRef, forwardRef,
Input, Input,
NgZone, NgZone,
OnInit, OnInit,
ViewChild ViewChild,
ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable } from 'rxjs';
import { import { debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
catchError,
debounceTime,
distinctUntilChanged,
map,
publishReplay,
refCount,
share,
switchMap,
tap
} from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state'; import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { BranchInfo } from '@shared/models/vc.models'; import { BranchInfo } from '@shared/models/vc.models';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service'; import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { isNotEmptyStr } from '@core/utils'; import { isNotEmptyStr } from '@core/utils';
import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete';
@Component({ @Component({
selector: 'tb-branch-autocomplete', selector: 'tb-branch-autocomplete',
templateUrl: './branch-autocomplete.component.html', templateUrl: './branch-autocomplete.component.html',
styleUrls: [], styleUrls: ['./branch-autocomplete.component.scss'],
providers: [{ providers: [{
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BranchAutocompleteComponent), useExisting: forwardRef(() => BranchAutocompleteComponent),
multi: true multi: true
}] }],
encapsulation: ViewEncapsulation.None
}) })
export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit { export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit {
@ -94,19 +86,21 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
@Input() @Input()
selectionMode = false; selectionMode = false;
@ViewChild('branchAutocomplete') matAutocomplete: MatAutocomplete;
@ViewChild('branchInput', { read: MatAutocompleteTrigger, static: true }) autoCompleteTrigger: MatAutocompleteTrigger;
@ViewChild('branchInput', {static: true}) branchInput: ElementRef; @ViewChild('branchInput', {static: true}) branchInput: ElementRef;
filteredBranches: Observable<Array<BranchInfo>>; filteredBranches: Observable<Array<BranchInfo>>;
branches: Observable<Array<BranchInfo>> = null;
defaultBranch: BranchInfo = null; defaultBranch: BranchInfo = null;
searchText = ''; searchText = '';
loading = false;
private dirty = false; private dirty = false;
private ignoreClosedPanel = false; private clearButtonClicked = false;
private propagateChange = (v: any) => { }; private propagateChange = (v: any) => { };
@ -140,7 +134,9 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
} else { } else {
modelValue = value; modelValue = value;
} }
this.updateView(modelValue); if (!this.selectionMode || modelValue) {
this.updateView(modelValue);
}
}), }),
map(value => { map(value => {
if (value) { if (value) {
@ -167,17 +163,33 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
this.disabled = isDisabled; 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) { if ((this.selectDefaultBranch && !this.modelValue) || force) {
this.getBranches(ignoreLoading).subscribe( setTimeout(() => {
(data) => { if (this.defaultBranch) {
if (this.defaultBranch || force) { this.branchFormGroup.get('branch').patchValue(this.defaultBranch, {emitEvent: false});
this.branchFormGroup.get('branch').patchValue(this.defaultBranch, {emitEvent: false}); this.modelValue = this.defaultBranch?.name;
this.modelValue = this.defaultBranch?.name; this.propagateChange(this.modelValue);
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() { onPanelClosed() {
if (this.ignoreClosedPanel) { this.selectAvailableValue();
this.ignoreClosedPanel = false; }
} else {
if (this.selectionMode && !this.branchFormGroup.get('branch').value && this.defaultBranch) { 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.zone.run(() => {
this.branchFormGroup.get('branch').patchValue(this.defaultBranch, {emitEvent: true}); this.branchFormGroup.get('branch').patchValue(val, {emitEvent: true});
}, 0); }, 0);
} });
} }
} }
@ -223,7 +257,7 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
return branch ? branch.name : undefined; return branch ? branch.name : undefined;
} }
fetchBranches(searchText?: string): Observable<Array<BranchInfo>> { private fetchBranches(searchText?: string): Observable<Array<BranchInfo>> {
this.searchText = searchText; this.searchText = searchText;
return this.getBranches().pipe( return this.getBranches().pipe(
map(branches => { map(branches => {
@ -239,25 +273,18 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
); );
} }
getBranches(ignoreLoading = true): Observable<Array<BranchInfo>> { private getBranches(): Observable<Array<BranchInfo>> {
if (!this.branches) { return this.entitiesVersionControlService.listBranches().pipe(
const branchesObservable = this.entitiesVersionControlService.listBranches({ignoreLoading, ignoreErrors: true}); tap((data) => {
this.branches = branchesObservable.pipe( this.defaultBranch = data.find(branch => branch.default);
catchError(() => of([] as Array<BranchInfo>)), })
tap((data) => { );
this.defaultBranch = data.find(branch => branch.default);
}),
publishReplay(1),
refCount()
);
}
return this.branches;
} }
clear() { clear() {
this.ignoreClosedPanel = true; this.clearButtonClicked = true;
this.branchFormGroup.get('branch').patchValue(null, {emitEvent: true});
setTimeout(() => { setTimeout(() => {
this.branchFormGroup.get('branch').patchValue(null, {emitEvent: true});
this.branchInput.nativeElement.blur(); this.branchInput.nativeElement.blur();
this.branchInput.nativeElement.focus(); this.branchInput.nativeElement.focus();
}, 0); }, 0);

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

@ -15,16 +15,26 @@
/// ///
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
export interface VersionCreateConfig { export interface VersionCreateConfig {
saveRelations: boolean; saveRelations: boolean;
} }
export interface VersionLoadConfig {
loadRelations: boolean;
}
export enum VersionCreateRequestType { export enum VersionCreateRequestType {
SINGLE_ENTITY = 'SINGLE_ENTITY', SINGLE_ENTITY = 'SINGLE_ENTITY',
COMPLEX = 'COMPLEX' COMPLEX = 'COMPLEX'
} }
export enum VersionLoadRequestType {
SINGLE_ENTITY = 'SINGLE_ENTITY',
ENTITY_TYPE = 'ENTITY_TYPE'
}
export interface VersionCreateRequest { export interface VersionCreateRequest {
versionName: string; versionName: string;
branch: string; branch: string;
@ -37,6 +47,18 @@ export interface SingleEntityVersionCreateRequest extends VersionCreateRequest {
type: VersionCreateRequestType.SINGLE_ENTITY; 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 { export interface BranchInfo {
name: string; name: string;
default: boolean; default: boolean;
@ -54,3 +76,10 @@ export interface VersionCreationResult {
modified: number; modified: number;
removed: 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", "download": "Download",
"next-with-label": "Next: {{label}}", "next-with-label": "Next: {{label}}",
"read-more": "Read more", "read-more": "Read more",
"hide": "Hide" "hide": "Hide",
"restore": "Restore"
}, },
"aggregation": { "aggregation": {
"aggregation": "Aggregation", "aggregation": "Aggregation",
@ -3112,21 +3113,25 @@
"version-control": "Version control", "version-control": "Version control",
"management": "Version control management", "management": "Version control management",
"branch": "Branch", "branch": "Branch",
"default": "Default",
"select-branch": "Select branch", "select-branch": "Select branch",
"branch-required": "Branch is required", "branch-required": "Branch is required",
"export-entity-version": "Export entity version", "create-entity-version": "Create entity version",
"entity-version-exported": "Entity version successfully exported",
"version-name": "Version name", "version-name": "Version name",
"version-name-required": "Version name is required", "version-name-required": "Version name is required",
"export-entity-relations": "Export entity relations", "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", "entity-versions": "Entity versions",
"versions": "Versions", "versions": "Versions",
"created-time": "Created time", "created-time": "Created time",
"version-id": "Version ID", "version-id": "Version ID",
"no-entity-versions-text": "No entity versions found", "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": {
"widget-library": "Widgets Library", "widget-library": "Widgets Library",

Loading…
Cancel
Save