Browse Source

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

pull/6616/head
Viacheslav Klimov 4 years ago
parent
commit
4c8b3bd7cb
  1. 111
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  2. 13
      application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java
  3. 6
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java
  4. 87
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
  5. 67
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java
  6. 102
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultTbVersionControlSettingsService.java
  7. 12
      application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java
  8. 9
      application/src/main/java/org/thingsboard/server/service/sync/vc/GitVersionControlQueueService.java
  9. 80
      application/src/main/java/org/thingsboard/server/service/sync/vc/TbAbstractVersionControlSettingsService.java
  10. 14
      application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/AutoCommitSettingsCaffeineCache.java
  11. 17
      application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/AutoCommitSettingsRedisCache.java
  12. 36
      application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/DefaultTbAutoCommitSettingsService.java
  13. 30
      application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/TbAutoCommitSettingsService.java
  14. 63
      application/src/main/java/org/thingsboard/server/service/sync/vc/repository/DefaultTbRepositorySettingsService.java
  15. 34
      application/src/main/java/org/thingsboard/server/service/sync/vc/repository/RepositorySettingsCaffeineCache.java
  16. 36
      application/src/main/java/org/thingsboard/server/service/sync/vc/repository/RepositorySettingsRedisCache.java
  17. 12
      application/src/main/java/org/thingsboard/server/service/sync/vc/repository/TbRepositorySettingsService.java
  18. 9
      application/src/main/resources/thingsboard.yml
  19. 10
      common/cluster-api/src/main/proto/queue.proto
  20. 3
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  21. 21
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java
  22. 27
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/AutoCommitSettings.java
  23. 28
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataInfo.java
  24. 1
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityVersion.java
  25. 2
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/RepositoryAuthMethod.java
  26. 16
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/RepositorySettings.java
  27. 10
      common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java
  28. 28
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java
  29. 27
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java
  30. 33
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java
  31. 8
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepositoryService.java
  32. 8
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/PendingCommit.java
  33. 6
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlRequestCtx.java
  34. 10
      ui-ngx/src/app/core/auth/auth.actions.ts
  35. 2
      ui-ngx/src/app/core/auth/auth.models.ts
  36. 4
      ui-ngx/src/app/core/auth/auth.reducer.ts
  37. 4
      ui-ngx/src/app/core/auth/auth.selectors.ts
  38. 10
      ui-ngx/src/app/core/auth/auth.service.ts
  39. 47
      ui-ngx/src/app/core/http/admin.service.ts
  40. 8
      ui-ngx/src/app/core/http/entities-version-control.service.ts
  41. 22
      ui-ngx/src/app/core/services/menu.service.ts
  42. 16
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  43. 125
      ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html
  44. 74
      ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.scss
  45. 210
      ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts
  46. 17
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html
  47. 4
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts
  48. 32
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html
  49. 44
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts
  50. 3
      ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html
  51. 6
      ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts
  52. 3
      ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html
  53. 6
      ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.ts
  54. 8
      ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.html
  55. 2
      ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.ts
  56. 41
      ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html
  57. 66
      ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.ts
  58. 26
      ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html
  59. 2
      ui-ngx/src/app/modules/home/components/vc/repository-settings.component.scss
  60. 216
      ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts
  61. 218
      ui-ngx/src/app/modules/home/components/vc/version-control-settings.component.ts
  62. 6
      ui-ngx/src/app/modules/home/components/vc/version-control.component.html
  63. 10
      ui-ngx/src/app/modules/home/components/vc/version-control.component.ts
  64. 24
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  65. 6
      ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
  66. 23
      ui-ngx/src/app/modules/home/pages/admin/auto-commit-admin-settings.component.html
  67. 51
      ui-ngx/src/app/modules/home/pages/admin/auto-commit-admin-settings.component.ts
  68. 2
      ui-ngx/src/app/modules/home/pages/admin/repository-admin-settings.component.html
  69. 12
      ui-ngx/src/app/modules/home/pages/admin/repository-admin-settings.component.ts
  70. 4
      ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html
  71. 3
      ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts
  72. 3
      ui-ngx/src/app/shared/models/constants.ts
  73. 19
      ui-ngx/src/app/shared/models/settings.models.ts
  74. 24
      ui-ngx/src/app/shared/models/vc.models.ts
  75. 32
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -29,24 +29,23 @@ 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;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.sms.config.TestSmsRequest; import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.sync.vc.EntitiesVersionControlSettings; import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.security.system.SystemSecurityService;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.sync.vc.autocommit.TbAutoCommitSettingsService;
import org.thingsboard.server.service.update.UpdateService; import org.thingsboard.server.service.update.UpdateService;
import static org.thingsboard.server.controller.ControllerConstants.*; import static org.thingsboard.server.controller.ControllerConstants.*;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID;
@RestController @RestController
@TbCoreComponent @TbCoreComponent
@ -68,6 +67,9 @@ public class AdminController extends BaseController {
@Autowired @Autowired
private EntitiesVersionControlService versionControlService; private EntitiesVersionControlService versionControlService;
@Autowired
private TbAutoCommitSettingsService autoCommitSettingsService;
@Autowired @Autowired
private UpdateService updateService; private UpdateService updateService;
@ -189,15 +191,15 @@ public class AdminController extends BaseController {
} }
} }
@ApiOperation(value = "Get version control settings (getVersionControlSettings)", @ApiOperation(value = "Get repository settings (getRepositorySettings)",
notes = "Get the version control settings object. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Get the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/vcSettings") @GetMapping("/repositorySettings")
@ResponseBody @ResponseBody
public EntitiesVersionControlSettings getVersionControlSettings() throws ThingsboardException { public RepositorySettings getRepositorySettings() throws ThingsboardException {
try { try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
EntitiesVersionControlSettings versionControlSettings = checkNotNull(versionControlService.getVersionControlSettings(getTenantId())); RepositorySettings versionControlSettings = checkNotNull(versionControlService.getVersionControlSettings(getTenantId()));
versionControlSettings.setPassword(null); versionControlSettings.setPassword(null);
versionControlSettings.setPrivateKey(null); versionControlSettings.setPrivateKey(null);
versionControlSettings.setPrivateKeyPassword(null); versionControlSettings.setPrivateKeyPassword(null);
@ -207,12 +209,12 @@ public class AdminController extends BaseController {
} }
} }
@ApiOperation(value = "Check version control settings exists (versionControlSettingsExists)", @ApiOperation(value = "Check repository settings exists (repositorySettingsExists)",
notes = "Check whether the version control settings exists. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Check whether the repository settings exists. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/vcSettings/exists") @GetMapping("/repositorySettings/exists")
@ResponseBody @ResponseBody
public Boolean versionControlSettingsExists() throws ThingsboardException { public Boolean repositorySettingsExists() throws ThingsboardException {
try { try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return versionControlService.getVersionControlSettings(getTenantId()) != null; return versionControlService.getVersionControlSettings(getTenantId()) != null;
@ -221,13 +223,13 @@ public class AdminController extends BaseController {
} }
} }
@ApiOperation(value = "Creates or Updates the version control settings (saveVersionControlSettings)", @ApiOperation(value = "Creates or Updates the repository settings (saveRepositorySettings)",
notes = "Creates or Updates the version control settings object. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Creates or Updates the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/vcSettings") @PostMapping("/repositorySettings")
public DeferredResult<EntitiesVersionControlSettings> saveVersionControlSettings(@RequestBody EntitiesVersionControlSettings settings) throws ThingsboardException { public DeferredResult<RepositorySettings> saveRepositorySettings(@RequestBody RepositorySettings settings) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
ListenableFuture<EntitiesVersionControlSettings> future = versionControlService.saveVersionControlSettings(getTenantId(), settings); ListenableFuture<RepositorySettings> future = versionControlService.saveVersionControlSettings(getTenantId(), settings);
return wrapFuture(Futures.transform(future, savedSettings -> { return wrapFuture(Futures.transform(future, savedSettings -> {
savedSettings.setPassword(null); savedSettings.setPassword(null);
savedSettings.setPrivateKey(null); savedSettings.setPrivateKey(null);
@ -236,13 +238,13 @@ public class AdminController extends BaseController {
}, MoreExecutors.directExecutor())); }, MoreExecutors.directExecutor()));
} }
@ApiOperation(value = "Delete version control settings (deleteVersionControlSettings)", @ApiOperation(value = "Delete repository settings (deleteRepositorySettings)",
notes = "Deletes the version control settings." notes = "Deletes the repository settings."
+ TENANT_AUTHORITY_PARAGRAPH) + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/vcSettings", method = RequestMethod.DELETE) @RequestMapping(value = "/repositorySettings", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public DeferredResult<Void> deleteVersionControlSettings() throws ThingsboardException { public DeferredResult<Void> deleteRepositorySettings() throws ThingsboardException {
try { try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.DELETE); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.DELETE);
return wrapFuture(versionControlService.deleteVersionControlSettings(getTenantId())); return wrapFuture(versionControlService.deleteVersionControlSettings(getTenantId()));
@ -251,13 +253,14 @@ public class AdminController extends BaseController {
} }
} }
@ApiOperation(value = "Check version control access (checkVersionControlAccess)",
notes = "Attempts to check version control access. " + TENANT_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Check repository access (checkRepositoryAccess)",
notes = "Attempts to check repository access. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/vcSettings/checkAccess", method = RequestMethod.POST) @RequestMapping(value = "/repositorySettings/checkAccess", method = RequestMethod.POST)
public DeferredResult<Void> checkVersionControlAccess( public DeferredResult<Void> checkRepositoryAccess(
@ApiParam(value = "A JSON value representing the Entities Version Control Settings.") @ApiParam(value = "A JSON value representing the Repository Settings.")
@RequestBody EntitiesVersionControlSettings settings) throws ThingsboardException { @RequestBody RepositorySettings 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);
@ -267,6 +270,58 @@ public class AdminController extends BaseController {
} }
} }
@ApiOperation(value = "Get auto commit settings (getAutoCommitSettings)",
notes = "Get the auto commit settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/autoCommitSettings")
@ResponseBody
public AutoCommitSettings getAutoCommitSettings() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return checkNotNull(autoCommitSettingsService.get(getTenantId()));
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Check auto commit settings exists (autoCommitSettingsExists)",
notes = "Check whether the auto commit settings exists. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/autoCommitSettings/exists")
@ResponseBody
public Boolean autoCommitSettingsExists() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return autoCommitSettingsService.get(getTenantId()) != null;
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Creates or Updates the auto commit settings (saveAutoCommitSettings)",
notes = "Creates or Updates the auto commit settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/autoCommitSettings")
public AutoCommitSettings saveAutoCommitSettings(@RequestBody AutoCommitSettings settings) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
return autoCommitSettingsService.save(getTenantId(), settings);
}
@ApiOperation(value = "Delete auto commit settings (deleteAutoCommitSettings)",
notes = "Deletes the auto commit settings."
+ TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/autoCommitSettings", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void deleteAutoCommitSettings() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.DELETE);
autoCommitSettingsService.delete(getTenantId());
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Check for new Platform Releases (checkUpdates)", @ApiOperation(value = "Check for new Platform Releases (checkUpdates)",
notes = "Check notifications about new platform releases. " notes = "Check notifications about new platform releases. "
+ SYSTEM_AUTHORITY_PARAGRAPH) + SYSTEM_AUTHORITY_PARAGRAPH)

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

@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.sync.vc.EntityDataDiff; import org.thingsboard.server.common.data.sync.vc.EntityDataDiff;
import org.thingsboard.server.common.data.sync.vc.EntityDataInfo;
import org.thingsboard.server.common.data.sync.vc.EntityVersion; import org.thingsboard.server.common.data.sync.vc.EntityVersion;
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.VersionLoadResult; import org.thingsboard.server.common.data.sync.vc.VersionLoadResult;
@ -237,6 +238,18 @@ public class EntitiesVersionControlController extends BaseController {
} }
} }
@GetMapping("/info/{versionId}/{entityType}/{internalEntityUuid}")
public DeferredResult<EntityDataInfo> getEntityDataInfo(@PathVariable String versionId,
@PathVariable EntityType entityType,
@PathVariable UUID internalEntityUuid) throws ThingsboardException {
try {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, internalEntityUuid);
return wrapFuture(versionControlService.getEntityDataInfo(getCurrentUser(), entityId, versionId));
} catch (Exception e) {
throw handleException(e);
}
}
@GetMapping("/diff/{branch}/{entityType}/{internalEntityUuid}") @GetMapping("/diff/{branch}/{entityType}/{internalEntityUuid}")
public DeferredResult<EntityDataDiff> compareEntityDataToVersion(@PathVariable String branch, public DeferredResult<EntityDataDiff> compareEntityDataToVersion(@PathVariable String branch,
@PathVariable EntityType entityType, @PathVariable EntityType entityType,

6
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java

@ -42,7 +42,11 @@ public class CustomerImportService extends BaseEntityImportService<CustomerId, C
@Override @Override
protected Customer prepareAndSave(TenantId tenantId, Customer customer, EntityExportData<Customer> exportData, IdProvider idProvider) { protected Customer prepareAndSave(TenantId tenantId, Customer customer, EntityExportData<Customer> exportData, IdProvider idProvider) {
return customerService.saveCustomer(customer); if (customer.isPublic()) {
return customerService.findOrCreatePublicCustomer(tenantId);
} else {
return customerService.saveCustomer(customer);
}
} }
@Override @Override

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

@ -44,7 +44,8 @@ import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportSettings; import org.thingsboard.server.common.data.sync.ie.EntityExportSettings;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult; import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.common.data.sync.ie.EntityImportSettings; import org.thingsboard.server.common.data.sync.ie.EntityImportSettings;
import org.thingsboard.server.common.data.sync.vc.EntitiesVersionControlSettings; import org.thingsboard.server.common.data.sync.vc.EntityDataInfo;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.EntityDataDiff; import org.thingsboard.server.common.data.sync.vc.EntityDataDiff;
import org.thingsboard.server.common.data.sync.vc.EntityVersion; import org.thingsboard.server.common.data.sync.vc.EntityVersion;
import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
@ -68,7 +69,9 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.sync.ie.EntitiesExportImportService; import org.thingsboard.server.service.sync.ie.EntitiesExportImportService;
import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService; import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService;
import org.thingsboard.server.service.sync.vc.autocommit.TbAutoCommitSettingsService;
import org.thingsboard.server.service.sync.vc.data.CommitGitRequest; import org.thingsboard.server.service.sync.vc.data.CommitGitRequest;
import org.thingsboard.server.service.sync.vc.repository.TbRepositorySettingsService;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
@ -92,7 +95,8 @@ import static com.google.common.util.concurrent.Futures.transformAsync;
@Slf4j @Slf4j
public class DefaultEntitiesVersionControlService implements EntitiesVersionControlService { public class DefaultEntitiesVersionControlService implements EntitiesVersionControlService {
private final TbVersionControlSettingsService vcSettingsService; private final TbRepositorySettingsService repositorySettingsService;
private final TbAutoCommitSettingsService autoCommitSettingsService;
private final GitVersionControlQueueService gitServiceQueue; private final GitVersionControlQueueService gitServiceQueue;
private final EntitiesExportImportService exportImportService; private final EntitiesExportImportService exportImportService;
private final ExportableEntitiesService exportableEntitiesService; private final ExportableEntitiesService exportableEntitiesService;
@ -119,7 +123,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@SuppressWarnings("UnstableApiUsage") @SuppressWarnings("UnstableApiUsage")
@Override @Override
public ListenableFuture<VersionCreationResult> saveEntitiesVersion(SecurityUser user, VersionCreateRequest request) throws Exception { public ListenableFuture<VersionCreationResult> saveEntitiesVersion(SecurityUser user, VersionCreateRequest request) throws Exception {
var pendingCommit = gitServiceQueue.prepareCommit(user.getTenantId(), request); var pendingCommit = gitServiceQueue.prepareCommit(user, request);
return transformAsync(pendingCommit, commit -> { return transformAsync(pendingCommit, commit -> {
List<ListenableFuture<Void>> gitFutures = new ArrayList<>(); List<ListenableFuture<Void>> gitFutures = new ArrayList<>();
@ -325,34 +329,42 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
EntityId externalId = ((ExportableEntity<EntityId>) entity).getExternalId(); EntityId externalId = ((ExportableEntity<EntityId>) entity).getExternalId();
if (externalId == null) externalId = entityId; if (externalId == null) externalId = entityId;
EntityExportData<?> currentVersion = exportImportService.exportEntity(user, entityId, EntityExportSettings.builder()
.exportRelations(true)
.exportAttributes(true)
.build());
return transformAsync(gitServiceQueue.getEntity(user.getTenantId(), versionId, externalId), return transformAsync(gitServiceQueue.getEntity(user.getTenantId(), versionId, externalId),
otherVersion -> transform(gitServiceQueue.getContentsDiff(user.getTenantId(), otherVersion -> {
JacksonUtil.toPrettyString(currentVersion), EntityExportData<?> currentVersion = exportImportService.exportEntity(user, entityId, EntityExportSettings.builder()
JacksonUtil.toPrettyString(otherVersion)), rawDiff -> { .exportRelations(otherVersion.getRelations() != null)
return new EntityDataDiff(currentVersion, otherVersion, rawDiff); .exportAttributes(otherVersion.getAttributes() != null)
}, MoreExecutors.directExecutor()), MoreExecutors.directExecutor()); .build());
return transform(gitServiceQueue.getContentsDiff(user.getTenantId(),
JacksonUtil.toPrettyString(currentVersion.sort()),
JacksonUtil.toPrettyString(otherVersion.sort())),
rawDiff -> new EntityDataDiff(currentVersion, otherVersion, rawDiff), MoreExecutors.directExecutor());
}, MoreExecutors.directExecutor());
} }
@Override
public ListenableFuture<EntityDataInfo> getEntityDataInfo(SecurityUser user, EntityId entityId, String versionId) {
return Futures.transform(gitServiceQueue.getEntity(user.getTenantId(), versionId, entityId),
entity -> new EntityDataInfo(entity.getRelations() != null, entity.getAttributes() != null), MoreExecutors.directExecutor());
}
@Override @Override
public ListenableFuture<List<String>> listBranches(TenantId tenantId) throws Exception { public ListenableFuture<List<String>> listBranches(TenantId tenantId) throws Exception {
return gitServiceQueue.listBranches(tenantId); return gitServiceQueue.listBranches(tenantId);
} }
@Override @Override
public EntitiesVersionControlSettings getVersionControlSettings(TenantId tenantId) { public RepositorySettings getVersionControlSettings(TenantId tenantId) {
return vcSettingsService.get(tenantId); return repositorySettingsService.get(tenantId);
} }
@Override @Override
public ListenableFuture<EntitiesVersionControlSettings> saveVersionControlSettings(TenantId tenantId, EntitiesVersionControlSettings versionControlSettings) { public ListenableFuture<RepositorySettings> saveVersionControlSettings(TenantId tenantId, RepositorySettings versionControlSettings) {
var restoredSettings = this.vcSettingsService.restore(tenantId, versionControlSettings); var restoredSettings = this.repositorySettingsService.restore(tenantId, versionControlSettings);
try { try {
var future = gitServiceQueue.initRepository(tenantId, restoredSettings); var future = gitServiceQueue.initRepository(tenantId, restoredSettings);
return Futures.transform(future, f -> vcSettingsService.save(tenantId, restoredSettings), MoreExecutors.directExecutor()); return Futures.transform(future, f -> repositorySettingsService.save(tenantId, restoredSettings), MoreExecutors.directExecutor());
} catch (Exception e) { } catch (Exception e) {
log.debug("{} Failed to init repository: {}", tenantId, versionControlSettings, e); log.debug("{} Failed to init repository: {}", tenantId, versionControlSettings, e);
throw new RuntimeException("Failed to init repository!", e); throw new RuntimeException("Failed to init repository!", e);
@ -361,7 +373,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@Override @Override
public ListenableFuture<Void> deleteVersionControlSettings(TenantId tenantId) throws Exception { public ListenableFuture<Void> deleteVersionControlSettings(TenantId tenantId) throws Exception {
if (vcSettingsService.delete(tenantId)) { if (repositorySettingsService.delete(tenantId)) {
return gitServiceQueue.clearRepository(tenantId); return gitServiceQueue.clearRepository(tenantId);
} else { } else {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
@ -369,8 +381,8 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
} }
@Override @Override
public ListenableFuture<Void> checkVersionControlAccess(TenantId tenantId, EntitiesVersionControlSettings settings) throws ThingsboardException { public ListenableFuture<Void> checkVersionControlAccess(TenantId tenantId, RepositorySettings settings) throws ThingsboardException {
settings = this.vcSettingsService.restore(tenantId, settings); settings = this.repositorySettingsService.restore(tenantId, settings);
try { try {
return gitServiceQueue.testRepository(tenantId, settings); return gitServiceQueue.testRepository(tenantId, settings);
} catch (Exception e) { } catch (Exception e) {
@ -381,22 +393,29 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@Override @Override
public ListenableFuture<VersionCreationResult> autoCommit(SecurityUser user, EntityId entityId) throws Exception { public ListenableFuture<VersionCreationResult> autoCommit(SecurityUser user, EntityId entityId) throws Exception {
var settings = vcSettingsService.get(user.getTenantId()); var repositorySettings = repositorySettingsService.get(user.getTenantId());
if (repositorySettings == null) {
return Futures.immediateFuture(null);
}
var autoCommitSettings = autoCommitSettingsService.get(user.getTenantId());
if (autoCommitSettings == null) {
return Futures.immediateFuture(null);
}
var entityType = entityId.getEntityType(); var entityType = entityId.getEntityType();
if (settings != null && settings.getAutoCommitSettings() != null && settings.getAutoCommitSettings().containsKey(entityType)) { AutoVersionCreateConfig autoCommitConfig = autoCommitSettings.get(entityType);
AutoVersionCreateConfig autoCommitConfig = settings.getAutoCommitSettings().get(entityType); if (autoCommitConfig == null) {
SingleEntityVersionCreateRequest vcr = new SingleEntityVersionCreateRequest(); return Futures.immediateFuture(null);
var autoCommitBranchName = autoCommitConfig.getBranch(); }
if (StringUtils.isEmpty(autoCommitBranchName)) { SingleEntityVersionCreateRequest vcr = new SingleEntityVersionCreateRequest();
autoCommitBranchName = StringUtils.isNotEmpty(settings.getDefaultBranch()) ? settings.getDefaultBranch() : "auto-commits"; var autoCommitBranchName = autoCommitConfig.getBranch();
} if (StringUtils.isEmpty(autoCommitBranchName)) {
vcr.setBranch(autoCommitBranchName); autoCommitBranchName = StringUtils.isNotEmpty(repositorySettings.getDefaultBranch()) ? repositorySettings.getDefaultBranch() : "auto-commits";
vcr.setVersionName("auto-commit by " + user.getEmail() + " at " + Instant.ofEpochSecond(System.currentTimeMillis() / 1000));
vcr.setEntityId(entityId);
vcr.setConfig(autoCommitConfig);
return saveEntitiesVersion(user, vcr);
} }
return Futures.immediateFuture(null); vcr.setBranch(autoCommitBranchName);
vcr.setVersionName("auto-commit at " + Instant.ofEpochSecond(System.currentTimeMillis() / 1000));
vcr.setEntityId(entityId);
vcr.setConfig(autoCommitConfig);
return saveEntitiesVersion(user, vcr);
} }
private String getCauseMessage(Exception e) { private String getCauseMessage(Exception e) {

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

@ -27,13 +27,14 @@ import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.vc.EntitiesVersionControlSettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.EntityVersion; import org.thingsboard.server.common.data.sync.vc.EntityVersion;
import org.thingsboard.server.common.data.sync.vc.EntityVersionsDiff; import org.thingsboard.server.common.data.sync.vc.EntityVersionsDiff;
import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
@ -66,10 +67,7 @@ import org.thingsboard.server.service.sync.vc.data.PendingGitRequest;
import org.thingsboard.server.service.sync.vc.data.VersionsDiffGitRequest; import org.thingsboard.server.service.sync.vc.data.VersionsDiffGitRequest;
import org.thingsboard.server.service.sync.vc.data.VoidGitRequest; import org.thingsboard.server.service.sync.vc.data.VoidGitRequest;
import java.util.HashMap; import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -96,12 +94,12 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
} }
@Override @Override
public ListenableFuture<CommitGitRequest> prepareCommit(TenantId tenantId, VersionCreateRequest request) { public ListenableFuture<CommitGitRequest> prepareCommit(User user, VersionCreateRequest request) {
SettableFuture<CommitGitRequest> future = SettableFuture.create(); SettableFuture<CommitGitRequest> future = SettableFuture.create();
CommitGitRequest commit = new CommitGitRequest(tenantId, request); CommitGitRequest commit = new CommitGitRequest(user.getTenantId(), request);
registerAndSend(commit, builder -> builder.setCommitRequest( registerAndSend(commit, builder -> builder.setCommitRequest(
buildCommitRequest(commit).setPrepareMsg(getCommitPrepareMsg(request)).build() buildCommitRequest(commit).setPrepareMsg(getCommitPrepareMsg(user, request)).build()
).build(), wrap(future, commit)); ).build(), wrap(future, commit));
return future; return future;
} }
@ -111,7 +109,7 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
SettableFuture<Void> future = SettableFuture.create(); SettableFuture<Void> future = SettableFuture.create();
String path = getRelativePath(entityData.getEntityType(), entityData.getEntity().getId()); String path = getRelativePath(entityData.getEntityType(), entityData.getEntity().getId());
String entityDataJson = JacksonUtil.toPrettyString(entityData); String entityDataJson = JacksonUtil.toPrettyString(entityData.sort());
registerAndSend(commit, builder -> builder.setCommitRequest( registerAndSend(commit, builder -> builder.setCommitRequest(
buildCommitRequest(commit).setAddMsg( buildCommitRequest(commit).setAddMsg(
@ -132,7 +130,7 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
buildCommitRequest(commit).setDeleteMsg( buildCommitRequest(commit).setDeleteMsg(
TransportProtos.DeleteMsg.newBuilder().setRelativePath(path).build() TransportProtos.DeleteMsg.newBuilder().setRelativePath(path).build()
).build() ).build()
).build(), wrap(commit.getFuture(), null)); ).build(), wrap(future, null));
return future; return future;
} }
@ -154,7 +152,7 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
return listVersions(tenantId, return listVersions(tenantId,
applyPageLinkParameters( applyPageLinkParameters(
ListVersionsRequestMsg.newBuilder() ListVersionsRequestMsg.newBuilder()
.setBranchName(branch), .setBranchName(branch),
pageLink pageLink
).build()); ).build());
} }
@ -164,8 +162,8 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
return listVersions(tenantId, return listVersions(tenantId,
applyPageLinkParameters( applyPageLinkParameters(
ListVersionsRequestMsg.newBuilder() ListVersionsRequestMsg.newBuilder()
.setBranchName(branch) .setBranchName(branch)
.setEntityType(entityType.name()), .setEntityType(entityType.name()),
pageLink pageLink
).build()); ).build());
} }
@ -175,10 +173,10 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
return listVersions(tenantId, return listVersions(tenantId,
applyPageLinkParameters( applyPageLinkParameters(
ListVersionsRequestMsg.newBuilder() ListVersionsRequestMsg.newBuilder()
.setBranchName(branch) .setBranchName(branch)
.setEntityType(entityId.getEntityType().name()) .setEntityType(entityId.getEntityType().name())
.setEntityIdMSB(entityId.getId().getMostSignificantBits()) .setEntityIdMSB(entityId.getId().getMostSignificantBits())
.setEntityIdLSB(entityId.getId().getLeastSignificantBits()), .setEntityIdLSB(entityId.getId().getLeastSignificantBits()),
pageLink pageLink
).build()); ).build());
} }
@ -271,7 +269,7 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
} }
private <T> void registerAndSend(PendingGitRequest<T> request, private <T> void registerAndSend(PendingGitRequest<T> request,
Function<ToVersionControlServiceMsg.Builder, ToVersionControlServiceMsg> enrichFunction, EntitiesVersionControlSettings settings, TbQueueCallback callback) { Function<ToVersionControlServiceMsg.Builder, ToVersionControlServiceMsg> enrichFunction, RepositorySettings settings, TbQueueCallback callback) {
if (!request.getFuture().isDone()) { if (!request.getFuture().isDone()) {
pendingRequestMap.putIfAbsent(request.getRequestId(), request); pendingRequestMap.putIfAbsent(request.getRequestId(), request);
var requestBody = enrichFunction.apply(newRequestProto(request, settings)); var requestBody = enrichFunction.apply(newRequestProto(request, settings));
@ -307,7 +305,7 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
} }
@Override @Override
public ListenableFuture<Void> initRepository(TenantId tenantId, EntitiesVersionControlSettings settings) { public ListenableFuture<Void> initRepository(TenantId tenantId, RepositorySettings settings) {
VoidGitRequest request = new VoidGitRequest(tenantId); VoidGitRequest request = new VoidGitRequest(tenantId);
registerAndSend(request, builder -> builder.setInitRepositoryRequest(GenericRepositoryRequestMsg.newBuilder().build()).build() registerAndSend(request, builder -> builder.setInitRepositoryRequest(GenericRepositoryRequestMsg.newBuilder().build()).build()
@ -317,7 +315,7 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
} }
@Override @Override
public ListenableFuture<Void> testRepository(TenantId tenantId, EntitiesVersionControlSettings settings) { public ListenableFuture<Void> testRepository(TenantId tenantId, RepositorySettings settings) {
VoidGitRequest request = new VoidGitRequest(tenantId); VoidGitRequest request = new VoidGitRequest(tenantId);
registerAndSend(request, builder -> builder registerAndSend(request, builder -> builder
@ -356,7 +354,9 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
} else if (vcResponseMsg.hasCommitResponse()) { } else if (vcResponseMsg.hasCommitResponse()) {
var commitResponse = vcResponseMsg.getCommitResponse(); var commitResponse = vcResponseMsg.getCommitResponse();
var commitResult = new VersionCreationResult(); var commitResult = new VersionCreationResult();
commitResult.setVersion(new EntityVersion(commitResponse.getTs(), commitResponse.getCommitId(), commitResponse.getName())); if (commitResponse.getTs() > 0) {
commitResult.setVersion(new EntityVersion(commitResponse.getTs(), commitResponse.getCommitId(), commitResponse.getName(), commitResponse.getAuthor()));
}
commitResult.setAdded(commitResponse.getAdded()); commitResult.setAdded(commitResponse.getAdded());
commitResult.setRemoved(commitResponse.getRemoved()); commitResult.setRemoved(commitResponse.getRemoved());
commitResult.setModified(commitResponse.getModified()); commitResult.setModified(commitResponse.getModified());
@ -405,7 +405,7 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
} }
private EntityVersion getEntityVersion(TransportProtos.EntityVersionProto proto) { private EntityVersion getEntityVersion(TransportProtos.EntityVersionProto proto) {
return new EntityVersion(proto.getTs(), proto.getId(), proto.getName()); return new EntityVersion(proto.getTs(), proto.getId(), proto.getName(), proto.getAuthor());
} }
private VersionedEntityInfo getVersionedEntityInfo(TransportProtos.VersionedEntityInfoProto proto) { private VersionedEntityInfo getVersionedEntityInfo(TransportProtos.VersionedEntityInfoProto proto) {
@ -453,11 +453,26 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
return path; return path;
} }
private static PrepareMsg getCommitPrepareMsg(VersionCreateRequest request) { private static PrepareMsg getCommitPrepareMsg(User user, VersionCreateRequest request) {
return PrepareMsg.newBuilder().setCommitMsg(request.getVersionName()).setBranchName(request.getBranch()).build(); return PrepareMsg.newBuilder().setCommitMsg(request.getVersionName())
.setBranchName(request.getBranch()).setAuthorName(getAuthorName(user)).setAuthorEmail(user.getEmail()).build();
}
private static String getAuthorName(User user) {
List<String> parts = new ArrayList<>();
if (StringUtils.isNotBlank(user.getFirstName())) {
parts.add(user.getFirstName());
}
if (StringUtils.isNotBlank(user.getLastName())) {
parts.add(user.getLastName());
}
if (parts.isEmpty()) {
parts.add(user.getName());
}
return String.join(" ", parts);
} }
private ToVersionControlServiceMsg.Builder newRequestProto(PendingGitRequest<?> request, EntitiesVersionControlSettings settings) { private ToVersionControlServiceMsg.Builder newRequestProto(PendingGitRequest<?> request, RepositorySettings settings) {
var tenantId = request.getTenantId(); var tenantId = request.getTenantId();
var requestId = request.getRequestId(); var requestId = request.getRequestId();
var builder = ToVersionControlServiceMsg.newBuilder() var builder = ToVersionControlServiceMsg.newBuilder()
@ -466,7 +481,7 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setRequestIdMSB(requestId.getMostSignificantBits()) .setRequestIdMSB(requestId.getMostSignificantBits())
.setRequestIdLSB(requestId.getLeastSignificantBits()); .setRequestIdLSB(requestId.getLeastSignificantBits());
EntitiesVersionControlSettings vcSettings = settings; RepositorySettings vcSettings = settings;
if (vcSettings == null && request.requiresSettings()) { if (vcSettings == null && request.requiresSettings()) {
vcSettings = entitiesVersionControlService.getVersionControlSettings(tenantId); vcSettings = entitiesVersionControlService.getVersionControlSettings(tenantId);
} }

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

@ -1,102 +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.
*/
package org.thingsboard.server.service.sync.vc;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.vc.EntitiesVersionControlSettings;
import org.thingsboard.server.common.data.sync.vc.VersionControlAuthMethod;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class DefaultTbVersionControlSettingsService implements TbVersionControlSettingsService {
public static final String SETTINGS_KEY = "entitiesVersionControl";
private final AdminSettingsService adminSettingsService;
private final TbTransactionalCache<TenantId, EntitiesVersionControlSettings> cache;
@Override
public EntitiesVersionControlSettings restore(TenantId tenantId, EntitiesVersionControlSettings settings) {
EntitiesVersionControlSettings storedSettings = get(tenantId);
if (storedSettings != null) {
VersionControlAuthMethod authMethod = settings.getAuthMethod();
if (VersionControlAuthMethod.USERNAME_PASSWORD.equals(authMethod) && settings.getPassword() == null) {
settings.setPassword(storedSettings.getPassword());
} else if (VersionControlAuthMethod.PRIVATE_KEY.equals(authMethod) && settings.getPrivateKey() == null) {
settings.setPrivateKey(storedSettings.getPrivateKey());
if (settings.getPrivateKeyPassword() == null) {
settings.setPrivateKeyPassword(storedSettings.getPrivateKeyPassword());
}
}
}
return settings;
}
@Override
public EntitiesVersionControlSettings get(TenantId tenantId) {
EntitiesVersionControlSettings settings = cache.getAndPutInTransaction(tenantId, () -> {
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, SETTINGS_KEY);
if (adminSettings != null) {
try {
return JacksonUtil.convertValue(adminSettings.getJsonValue(), EntitiesVersionControlSettings.class);
} catch (Exception e) {
throw new RuntimeException("Failed to load version control settings!", e);
}
}
return null;
}, true);
if (settings != null) {
settings = new EntitiesVersionControlSettings(settings);
}
return settings;
}
@Override
public EntitiesVersionControlSettings save(TenantId tenantId, EntitiesVersionControlSettings versionControlSettings) {
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, SETTINGS_KEY);
if (adminSettings == null) {
adminSettings = new AdminSettings();
adminSettings.setKey(SETTINGS_KEY);
adminSettings.setTenantId(tenantId);
}
adminSettings.setJsonValue(JacksonUtil.valueToTree(versionControlSettings));
AdminSettings savedAdminSettings = adminSettingsService.saveAdminSettings(tenantId, adminSettings);
EntitiesVersionControlSettings savedVersionControlSettings;
try {
savedVersionControlSettings = JacksonUtil.convertValue(savedAdminSettings.getJsonValue(), EntitiesVersionControlSettings.class);
} catch (Exception e) {
throw new RuntimeException("Failed to load version control settings!", e);
}
//API calls to adminSettingsService are not in transaction, so we can simply evict the cache.
cache.evict(tenantId);
return savedVersionControlSettings;
}
@Override
public boolean delete(TenantId tenantId) {
boolean result = adminSettingsService.deleteAdminSettings(tenantId, SETTINGS_KEY);
cache.evict(tenantId);
return result;
}
}

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

@ -16,15 +16,15 @@
package org.thingsboard.server.service.sync.vc; package org.thingsboard.server.service.sync.vc;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
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.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.sync.vc.EntityDataDiff; import org.thingsboard.server.common.data.sync.vc.EntityDataDiff;
import org.thingsboard.server.common.data.sync.vc.EntityDataInfo;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.common.data.sync.vc.EntitiesVersionControlSettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.EntityVersion; import org.thingsboard.server.common.data.sync.vc.EntityVersion;
import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
import org.thingsboard.server.common.data.sync.vc.VersionLoadResult; import org.thingsboard.server.common.data.sync.vc.VersionLoadResult;
@ -54,13 +54,15 @@ public interface EntitiesVersionControlService {
ListenableFuture<List<String>> listBranches(TenantId tenantId) throws Exception; ListenableFuture<List<String>> listBranches(TenantId tenantId) throws Exception;
EntitiesVersionControlSettings getVersionControlSettings(TenantId tenantId); RepositorySettings getVersionControlSettings(TenantId tenantId);
ListenableFuture<EntitiesVersionControlSettings> saveVersionControlSettings(TenantId tenantId, EntitiesVersionControlSettings versionControlSettings); ListenableFuture<RepositorySettings> saveVersionControlSettings(TenantId tenantId, RepositorySettings versionControlSettings);
ListenableFuture<Void> deleteVersionControlSettings(TenantId tenantId) throws Exception; ListenableFuture<Void> deleteVersionControlSettings(TenantId tenantId) throws Exception;
ListenableFuture<Void> checkVersionControlAccess(TenantId tenantId, EntitiesVersionControlSettings settings) throws Exception; ListenableFuture<Void> checkVersionControlAccess(TenantId tenantId, RepositorySettings settings) throws Exception;
ListenableFuture<VersionCreationResult> autoCommit(SecurityUser user, EntityId entityId) throws Exception; ListenableFuture<VersionCreationResult> autoCommit(SecurityUser user, EntityId entityId) throws Exception;
ListenableFuture<EntityDataInfo> getEntityDataInfo(SecurityUser user, EntityId entityId, String versionId);
} }

9
application/src/main/java/org/thingsboard/server/service/sync/vc/GitVersionControlQueueService.java

@ -18,12 +18,13 @@ package org.thingsboard.server.service.sync.vc;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.User;
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.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.vc.EntitiesVersionControlSettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.EntityVersion; import org.thingsboard.server.common.data.sync.vc.EntityVersion;
import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo; import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo;
@ -36,7 +37,7 @@ import java.util.List;
public interface GitVersionControlQueueService { public interface GitVersionControlQueueService {
ListenableFuture<CommitGitRequest> prepareCommit(TenantId tenantId, VersionCreateRequest request); ListenableFuture<CommitGitRequest> prepareCommit(User user, VersionCreateRequest request);
ListenableFuture<Void> addToCommit(CommitGitRequest commit, EntityExportData<ExportableEntity<EntityId>> entityData); ListenableFuture<Void> addToCommit(CommitGitRequest commit, EntityExportData<ExportableEntity<EntityId>> entityData);
@ -64,9 +65,9 @@ public interface GitVersionControlQueueService {
ListenableFuture<String> getContentsDiff(TenantId tenantId, String rawEntityData1, String rawEntityData2); ListenableFuture<String> getContentsDiff(TenantId tenantId, String rawEntityData1, String rawEntityData2);
ListenableFuture<Void> initRepository(TenantId tenantId, EntitiesVersionControlSettings settings); ListenableFuture<Void> initRepository(TenantId tenantId, RepositorySettings settings);
ListenableFuture<Void> testRepository(TenantId tenantId, EntitiesVersionControlSettings settings); ListenableFuture<Void> testRepository(TenantId tenantId, RepositorySettings settings);
ListenableFuture<Void> clearRepository(TenantId tenantId); ListenableFuture<Void> clearRepository(TenantId tenantId);

80
application/src/main/java/org/thingsboard/server/service/sync/vc/TbAbstractVersionControlSettingsService.java

@ -0,0 +1,80 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sync.vc;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import java.io.Serializable;
public abstract class TbAbstractVersionControlSettingsService<T extends Serializable> {
private final String settingsKey;
private final AdminSettingsService adminSettingsService;
private final TbTransactionalCache<TenantId, T> cache;
private final Class<T> clazz;
public TbAbstractVersionControlSettingsService(AdminSettingsService adminSettingsService, TbTransactionalCache<TenantId, T> cache, Class<T> clazz, String settingsKey) {
this.adminSettingsService = adminSettingsService;
this.cache = cache;
this.clazz = clazz;
this.settingsKey = settingsKey;
}
public T get(TenantId tenantId) {
return cache.getAndPutInTransaction(tenantId, () -> {
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, settingsKey);
if (adminSettings != null) {
try {
return JacksonUtil.convertValue(adminSettings.getJsonValue(), clazz);
} catch (Exception e) {
throw new RuntimeException("Failed to load " + settingsKey + " settings!", e);
}
}
return null;
}, true);
}
public T save(TenantId tenantId, T settings) {
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, settingsKey);
if (adminSettings == null) {
adminSettings = new AdminSettings();
adminSettings.setKey(settingsKey);
adminSettings.setTenantId(tenantId);
}
adminSettings.setJsonValue(JacksonUtil.valueToTree(settings));
AdminSettings savedAdminSettings = adminSettingsService.saveAdminSettings(tenantId, adminSettings);
T savedSettings;
try {
savedSettings = JacksonUtil.convertValue(savedAdminSettings.getJsonValue(), clazz);
} catch (Exception e) {
throw new RuntimeException("Failed to load auto commit settings!", e);
}
//API calls to adminSettingsService are not in transaction, so we can simply evict the cache.
cache.evict(tenantId);
return savedSettings;
}
public boolean delete(TenantId tenantId) {
boolean result = adminSettingsService.deleteAdminSettings(tenantId, settingsKey);
cache.evict(tenantId);
return result;
}
}

14
application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlSettingsCaffeineCache.java → application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/AutoCommitSettingsCaffeineCache.java

@ -13,24 +13,22 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.service.sync.vc; package org.thingsboard.server.service.sync.vc.autocommit;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache; import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.vc.EntitiesVersionControlSettings; import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
import org.thingsboard.server.gen.transport.TransportProtos;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("VersionControlCache") @Service("AutoCommitSettingsCache")
public class VersionControlSettingsCaffeineCache extends CaffeineTbTransactionalCache<TenantId, EntitiesVersionControlSettings> { public class AutoCommitSettingsCaffeineCache extends CaffeineTbTransactionalCache<TenantId, AutoCommitSettings> {
public VersionControlSettingsCaffeineCache(CacheManager cacheManager) { public AutoCommitSettingsCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.VC_SETTINGS_CACHE); super(cacheManager, CacheConstants.AUTO_COMMIT_SETTINGS_CACHE);
} }
} }

17
application/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlSettingsRedisCache.java → application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/AutoCommitSettingsRedisCache.java

@ -13,29 +13,24 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.service.sync.vc; package org.thingsboard.server.service.sync.vc.autocommit;
import com.google.protobuf.InvalidProtocolBufferException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache; import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbRedisSerializer; import org.thingsboard.server.cache.TbRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.vc.EntitiesVersionControlSettings; import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
import org.thingsboard.server.gen.transport.TransportProtos;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("VersionControlCache") @Service("AutoCommitSettingsCache")
public class VersionControlSettingsRedisCache extends RedisTbTransactionalCache<TenantId, EntitiesVersionControlSettings> { public class AutoCommitSettingsRedisCache extends RedisTbTransactionalCache<TenantId, AutoCommitSettings> {
public VersionControlSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { public AutoCommitSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.VC_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>()); super(CacheConstants.AUTO_COMMIT_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>());
} }
} }

36
application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/DefaultTbAutoCommitSettingsService.java

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sync.vc.autocommit;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.TbAbstractVersionControlSettingsService;
@Service
@TbCoreComponent
public class DefaultTbAutoCommitSettingsService extends TbAbstractVersionControlSettingsService<AutoCommitSettings> implements TbAutoCommitSettingsService {
public static final String SETTINGS_KEY = "autoCommitSettings";
public DefaultTbAutoCommitSettingsService(AdminSettingsService adminSettingsService, TbTransactionalCache<TenantId, AutoCommitSettings> cache) {
super(adminSettingsService, cache, AutoCommitSettings.class, SETTINGS_KEY);
}
}

30
application/src/main/java/org/thingsboard/server/service/sync/vc/autocommit/TbAutoCommitSettingsService.java

@ -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.
*/
package org.thingsboard.server.service.sync.vc.autocommit;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
public interface TbAutoCommitSettingsService {
AutoCommitSettings get(TenantId tenantId);
AutoCommitSettings save(TenantId tenantId, AutoCommitSettings settings);
boolean delete(TenantId tenantId);
}

63
application/src/main/java/org/thingsboard/server/service/sync/vc/repository/DefaultTbRepositorySettingsService.java

@ -0,0 +1,63 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sync.vc.repository;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.RepositoryAuthMethod;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.TbAbstractVersionControlSettingsService;
@Service
@TbCoreComponent
public class DefaultTbRepositorySettingsService extends TbAbstractVersionControlSettingsService<RepositorySettings> implements TbRepositorySettingsService {
public static final String SETTINGS_KEY = "entitiesVersionControl";
public DefaultTbRepositorySettingsService(AdminSettingsService adminSettingsService, TbTransactionalCache<TenantId, RepositorySettings> cache) {
super(adminSettingsService, cache, RepositorySettings.class, SETTINGS_KEY);
}
@Override
public RepositorySettings restore(TenantId tenantId, RepositorySettings settings) {
RepositorySettings storedSettings = get(tenantId);
if (storedSettings != null) {
RepositoryAuthMethod authMethod = settings.getAuthMethod();
if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(authMethod) && settings.getPassword() == null) {
settings.setPassword(storedSettings.getPassword());
} else if (RepositoryAuthMethod.PRIVATE_KEY.equals(authMethod) && settings.getPrivateKey() == null) {
settings.setPrivateKey(storedSettings.getPrivateKey());
if (settings.getPrivateKeyPassword() == null) {
settings.setPrivateKeyPassword(storedSettings.getPrivateKeyPassword());
}
}
}
return settings;
}
@Override
public RepositorySettings get(TenantId tenantId) {
RepositorySettings settings = super.get(tenantId);
if (settings != null) {
settings = new RepositorySettings(settings);
}
return settings;
}
}

34
application/src/main/java/org/thingsboard/server/service/sync/vc/repository/RepositorySettingsCaffeineCache.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sync.vc.repository;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("RepositorySettingsCache")
public class RepositorySettingsCaffeineCache extends CaffeineTbTransactionalCache<TenantId, RepositorySettings> {
public RepositorySettingsCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.REPOSITORY_SETTINGS_CACHE);
}
}

36
application/src/main/java/org/thingsboard/server/service/sync/vc/repository/RepositorySettingsRedisCache.java

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sync.vc.repository;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("RepositorySettingsCache")
public class RepositorySettingsRedisCache extends RedisTbTransactionalCache<TenantId, RepositorySettings> {
public RepositorySettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.REPOSITORY_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>());
}
}

12
application/src/main/java/org/thingsboard/server/service/sync/vc/TbVersionControlSettingsService.java → application/src/main/java/org/thingsboard/server/service/sync/vc/repository/TbRepositorySettingsService.java

@ -13,18 +13,18 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.service.sync.vc; package org.thingsboard.server.service.sync.vc.repository;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.vc.EntitiesVersionControlSettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
public interface TbVersionControlSettingsService { public interface TbRepositorySettingsService {
EntitiesVersionControlSettings restore(TenantId tenantId, EntitiesVersionControlSettings versionControlSettings); RepositorySettings restore(TenantId tenantId, RepositorySettings versionControlSettings);
EntitiesVersionControlSettings get(TenantId tenantId); RepositorySettings get(TenantId tenantId);
EntitiesVersionControlSettings save(TenantId tenantId, EntitiesVersionControlSettings versionControlSettings); RepositorySettings save(TenantId tenantId, RepositorySettings versionControlSettings);
boolean delete(TenantId tenantId); boolean delete(TenantId tenantId);

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

@ -433,9 +433,12 @@ cache:
edges: edges:
timeToLiveInMinutes: "${CACHE_SPECS_EDGES_TTL:1440}" timeToLiveInMinutes: "${CACHE_SPECS_EDGES_TTL:1440}"
maxSize: "${CACHE_SPECS_EDGES_MAX_SIZE:10000}" maxSize: "${CACHE_SPECS_EDGES_MAX_SIZE:10000}"
vcSettings: repositorySettings:
timeToLiveInMinutes: "${CACHE_SPECS_VC_SETTINGS_TTL:1440}" timeToLiveInMinutes: "${CACHE_SPECS_REPOSITORY_SETTINGS_TTL:1440}"
maxSize: "${CACHE_SPECS_VC_SETTINGS_MAX_SIZE:10000}" maxSize: "${CACHE_SPECS_REPOSITORY_SETTINGS_MAX_SIZE:10000}"
autoCommitSettings:
timeToLiveInMinutes: "${CACHE_SPECS_AUTO_COMMIT_SETTINGS_TTL:1440}"
maxSize: "${CACHE_SPECS_AUTO_COMMIT_SETTINGS_MAX_SIZE:10000}"
redis: redis:
# standalone or cluster # standalone or cluster

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

@ -692,14 +692,17 @@ message CommitResponseMsg {
int64 ts = 1; int64 ts = 1;
string commitId = 2; string commitId = 2;
string name = 3; string name = 3;
int32 added = 4; string author = 4;
int32 modified = 5; int32 added = 5;
int32 removed = 6; int32 modified = 6;
int32 removed = 7;
} }
message PrepareMsg { message PrepareMsg {
string commitMsg = 1; string commitMsg = 1;
string branchName = 2; string branchName = 2;
string authorName = 3;
string authorEmail = 4;
} }
message AddMsg { message AddMsg {
@ -733,6 +736,7 @@ message EntityVersionProto {
int64 ts = 1; int64 ts = 1;
string id = 2; string id = 2;
string name = 3; string name = 3;
string author = 4;
} }
message ListVersionsResponseMsg { message ListVersionsResponseMsg {

3
common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

@ -31,5 +31,6 @@ public class CacheConstants {
public static final String TOKEN_OUTDATAGE_TIME_CACHE = "tokensOutdatageTime"; public static final String TOKEN_OUTDATAGE_TIME_CACHE = "tokensOutdatageTime";
public static final String OTA_PACKAGE_CACHE = "otaPackages"; public static final String OTA_PACKAGE_CACHE = "otaPackages";
public static final String OTA_PACKAGE_DATA_CACHE = "otaPackagesData"; public static final String OTA_PACKAGE_DATA_CACHE = "otaPackagesData";
public static final String VC_SETTINGS_CACHE = "vcSettings"; public static final String REPOSITORY_SETTINGS_CACHE = "repositorySettings";
public static final String AUTO_COMMIT_SETTINGS_CACHE = "autoCommitSettings";
} }

21
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java

@ -28,6 +28,8 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.sync.JsonTbEntity; import org.thingsboard.server.common.data.sync.JsonTbEntity;
import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -41,6 +43,15 @@ import java.util.Map;
@Data @Data
public class EntityExportData<E extends ExportableEntity<? extends EntityId>> { public class EntityExportData<E extends ExportableEntity<? extends EntityId>> {
public static final Comparator<EntityRelation> relationsComparator = Comparator
.comparing(EntityRelation::getFrom, Comparator.comparing(EntityId::getId))
.thenComparing(EntityRelation::getTo, Comparator.comparing(EntityId::getId))
.thenComparing(EntityRelation::getTypeGroup)
.thenComparing(EntityRelation::getType);
public static final Comparator<AttributeExportData> attrComparator = Comparator
.comparing(AttributeExportData::getKey).thenComparing(AttributeExportData::getLastUpdateTs);
@JsonTbEntity @JsonTbEntity
private E entity; private E entity;
private EntityType entityType; private EntityType entityType;
@ -48,4 +59,14 @@ public class EntityExportData<E extends ExportableEntity<? extends EntityId>> {
private List<EntityRelation> relations; private List<EntityRelation> relations;
private Map<String, List<AttributeExportData>> attributes; private Map<String, List<AttributeExportData>> attributes;
public EntityExportData<E> sort() {
if (relations != null && !relations.isEmpty()) {
relations.sort(relationsComparator);
}
if (attributes != null && !attributes.isEmpty()) {
attributes.values().forEach(list -> list.sort(attrComparator));
}
return this;
}
} }

27
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/AutoCommitSettings.java

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.sync.vc;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.sync.vc.request.create.AutoVersionCreateConfig;
import java.util.HashMap;
public class AutoCommitSettings extends HashMap<EntityType, AutoVersionCreateConfig> {
private static final long serialVersionUID = -5757067601838792059L;
}

28
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataInfo.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.sync.vc;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EntityDataInfo {
boolean hasRelations;
boolean hasAttributes;
}

1
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityVersion.java

@ -26,4 +26,5 @@ public class EntityVersion {
private long timestamp; private long timestamp;
private String id; private String id;
private String name; private String name;
private String author;
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/VersionControlAuthMethod.java → common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/RepositoryAuthMethod.java

@ -15,7 +15,7 @@
*/ */
package org.thingsboard.server.common.data.sync.vc; package org.thingsboard.server.common.data.sync.vc;
public enum VersionControlAuthMethod { public enum RepositoryAuthMethod {
USERNAME_PASSWORD, USERNAME_PASSWORD,
PRIVATE_KEY PRIVATE_KEY
} }

16
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntitiesVersionControlSettings.java → common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/RepositorySettings.java

@ -15,20 +15,17 @@
*/ */
package org.thingsboard.server.common.data.sync.vc; package org.thingsboard.server.common.data.sync.vc;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.sync.vc.request.create.AutoVersionCreateConfig;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
@Data @Data
public class EntitiesVersionControlSettings implements Serializable { public class RepositorySettings implements Serializable {
private static final long serialVersionUID = -3211552851889198721L; private static final long serialVersionUID = -3211552851889198721L;
private String repositoryUri; private String repositoryUri;
private VersionControlAuthMethod authMethod; private RepositoryAuthMethod authMethod;
private String username; private String username;
private String password; private String password;
private String privateKeyFileName; private String privateKeyFileName;
@ -36,12 +33,10 @@ public class EntitiesVersionControlSettings implements Serializable {
private String privateKeyPassword; private String privateKeyPassword;
private String defaultBranch; private String defaultBranch;
private Map<EntityType, AutoVersionCreateConfig> autoCommitSettings; public RepositorySettings() {
public EntitiesVersionControlSettings() {
} }
public EntitiesVersionControlSettings(EntitiesVersionControlSettings settings) { public RepositorySettings(RepositorySettings settings) {
this.repositoryUri = settings.getRepositoryUri(); this.repositoryUri = settings.getRepositoryUri();
this.authMethod = settings.getAuthMethod(); this.authMethod = settings.getAuthMethod();
this.username = settings.getUsername(); this.username = settings.getUsername();
@ -50,6 +45,5 @@ public class EntitiesVersionControlSettings implements Serializable {
this.privateKey = settings.getPrivateKey(); this.privateKey = settings.getPrivateKey();
this.privateKeyPassword = settings.getPrivateKeyPassword(); this.privateKeyPassword = settings.getPrivateKeyPassword();
this.defaultBranch = settings.getDefaultBranch(); this.defaultBranch = settings.getDefaultBranch();
this.autoCommitSettings = settings.getAutoCommitSettings() != null ? new HashMap<>(settings.getAutoCommitSettings()) : new HashMap<>();
} }
} }

10
common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java

@ -18,8 +18,10 @@ package org.thingsboard.common.util;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException; import java.io.IOException;
@ -31,8 +33,10 @@ import java.util.Arrays;
public class JacksonUtil { public class JacksonUtil {
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public static final ObjectMapper PRETTY_JSON_MAPPER = new ObjectMapper() public static final ObjectMapper PRETTY_SORTED_JSON_MAPPER = JsonMapper.builder().enable(SerializationFeature.INDENT_OUTPUT)
.enable(SerializationFeature.INDENT_OUTPUT); .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true)
.build();
public static <T> T convertValue(Object fromValue, Class<T> toValueType) { public static <T> T convertValue(Object fromValue, Class<T> toValueType) {
try { try {
@ -99,7 +103,7 @@ public class JacksonUtil {
public static String toPrettyString(Object o) { public static String toPrettyString(Object o) {
try { try {
return PRETTY_JSON_MAPPER.writeValueAsString(o); return PRETTY_SORTED_JSON_MAPPER.writeValueAsString(o);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

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

@ -35,7 +35,7 @@ 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.page.SortOrder;
import org.thingsboard.server.common.data.sync.vc.EntitiesVersionControlSettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
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;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
@ -312,7 +312,7 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
.setTotalElements(data.getTotalElements()) .setTotalElements(data.getTotalElements())
.setHasNext(data.hasNext()) .setHasNext(data.hasNext())
.addAllVersions(data.getData().stream().map( .addAllVersions(data.getData().stream().map(
v -> EntityVersionProto.newBuilder().setTs(v.getTimestamp()).setId(v.getId()).setName(v.getName()).build() v -> EntityVersionProto.newBuilder().setTs(v.getTimestamp()).setId(v.getId()).setName(v.getName()).setAuthor(v.getAuthor()).build()
).collect(Collectors.toList()))) ).collect(Collectors.toList())))
); );
} }
@ -397,7 +397,8 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
private void prepareCommit(VersionControlRequestCtx ctx, UUID txId, PrepareMsg prepareMsg) { private void prepareCommit(VersionControlRequestCtx ctx, UUID txId, PrepareMsg prepareMsg) {
var tenantId = ctx.getTenantId(); var tenantId = ctx.getTenantId();
var pendingCommit = new PendingCommit(tenantId, ctx.getNodeId(), txId, prepareMsg.getBranchName(), prepareMsg.getCommitMsg()); var pendingCommit = new PendingCommit(tenantId, ctx.getNodeId(), txId, prepareMsg.getBranchName(),
prepareMsg.getCommitMsg(), prepareMsg.getAuthorName(), prepareMsg.getAuthorEmail());
PendingCommit old = pendingCommitMap.get(tenantId); PendingCommit old = pendingCommitMap.get(tenantId);
if (old != null) { if (old != null) {
doAbortCurrentCommit(tenantId, old); doAbortCurrentCommit(tenantId, old);
@ -456,13 +457,18 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
} }
private void reply(VersionControlRequestCtx ctx, VersionCreationResult result) { private void reply(VersionControlRequestCtx ctx, VersionCreationResult result) {
reply(ctx, Optional.empty(), builder -> builder.setCommitResponse(CommitResponseMsg.newBuilder() var responseBuilder = CommitResponseMsg.newBuilder().setAdded(result.getAdded())
.setTs(result.getVersion().getTimestamp())
.setCommitId(result.getVersion().getId())
.setName(result.getVersion().getName())
.setAdded(result.getAdded())
.setModified(result.getModified()) .setModified(result.getModified())
.setRemoved(result.getRemoved()))); .setRemoved(result.getRemoved());
if (result.getVersion() != null) {
responseBuilder.setTs(result.getVersion().getTimestamp())
.setCommitId(result.getVersion().getId())
.setName(result.getVersion().getName())
.setAuthor(result.getVersion().getAuthor());
}
reply(ctx, Optional.empty(), builder -> builder.setCommitResponse(responseBuilder));
} }
private void reply(VersionControlRequestCtx ctx, Optional<Exception> e) { private void reply(VersionControlRequestCtx ctx, Optional<Exception> e) {
@ -495,8 +501,8 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
producer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null); producer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null);
} }
private EntitiesVersionControlSettings getEntitiesVersionControlSettings(ToVersionControlServiceMsg msg) { private RepositorySettings getEntitiesVersionControlSettings(ToVersionControlServiceMsg msg) {
Optional<EntitiesVersionControlSettings> settingsOpt = encodingService.decode(msg.getVcSettings().toByteArray()); Optional<RepositorySettings> settingsOpt = encodingService.decode(msg.getVcSettings().toByteArray());
if (settingsOpt.isPresent()) { if (settingsOpt.isPresent()) {
return settingsOpt.get(); return settingsOpt.get();
} else { } else {

27
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java

@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.sync.vc.EntitiesVersionControlSettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.EntityVersion; import org.thingsboard.server.common.data.sync.vc.EntityVersion;
import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo; import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo;
@ -117,10 +117,11 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
result.setModified(status.getModified().size()); result.setModified(status.getModified().size());
result.setRemoved(status.getRemoved().size()); result.setRemoved(status.getRemoved().size());
GitRepository.Commit gitCommit = repository.commit(commit.getVersionName()); if (result.getAdded() > 0 || result.getModified() > 0 || result.getRemoved() > 0) {
repository.push(commit.getWorkingBranch(), commit.getBranch()); GitRepository.Commit gitCommit = repository.commit(commit.getVersionName(), commit.getAuthorName(), commit.getAuthorEmail());
repository.push(commit.getWorkingBranch(), commit.getBranch());
result.setVersion(toVersion(gitCommit)); result.setVersion(toVersion(gitCommit));
}
return result; return result;
} catch (GitAPIException gitAPIException) { } catch (GitAPIException gitAPIException) {
//TODO: analyze and return meaningful exceptions that we can show to the client; //TODO: analyze and return meaningful exceptions that we can show to the client;
@ -216,13 +217,13 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
} }
@Override @Override
public void testRepository(TenantId tenantId, EntitiesVersionControlSettings settings) throws Exception { public void testRepository(TenantId tenantId, RepositorySettings settings) throws Exception {
Path repositoryDirectory = Path.of(repositoriesFolder, tenantId.getId().toString()); Path repositoryDirectory = Path.of(repositoriesFolder, tenantId.getId().toString());
GitRepository.test(settings, repositoryDirectory.toFile()); GitRepository.test(settings, repositoryDirectory.toFile());
} }
@Override @Override
public void initRepository(TenantId tenantId, EntitiesVersionControlSettings settings) throws Exception { public void initRepository(TenantId tenantId, RepositorySettings settings) throws Exception {
clearRepository(tenantId); clearRepository(tenantId);
log.debug("[{}] Init tenant repository started.", tenantId); log.debug("[{}] Init tenant repository started.", tenantId);
Path repositoryDirectory = Path.of(repositoriesFolder, tenantId.getId().toString()); Path repositoryDirectory = Path.of(repositoriesFolder, tenantId.getId().toString());
@ -238,7 +239,7 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
} }
@Override @Override
public EntitiesVersionControlSettings getRepositorySettings(TenantId tenantId) throws Exception { public RepositorySettings getRepositorySettings(TenantId tenantId) throws Exception {
var gitRepository = repositories.get(tenantId); var gitRepository = repositories.get(tenantId);
return gitRepository != null ? gitRepository.getSettings() : null; return gitRepository != null ? gitRepository.getSettings() : null;
} }
@ -255,7 +256,15 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
} }
private EntityVersion toVersion(GitRepository.Commit commit) { private EntityVersion toVersion(GitRepository.Commit commit) {
return new EntityVersion(commit.getTimestamp(), commit.getId(), commit.getMessage()); return new EntityVersion(commit.getTimestamp(), commit.getId(), commit.getMessage(), this.getAuthor(commit));
}
private String getAuthor(GitRepository.Commit commit) {
String author = String.format("<%s>", commit.getAuthorEmail());
if (StringUtils.isNotBlank(commit.getAuthorName())) {
author = String.format("%s %s", commit.getAuthorName(), author);
}
return author;
} }
public static EntityId fromRelativePath(String path) { public static EntityId fromRelativePath(String path) {

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

@ -58,8 +58,8 @@ import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
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.page.SortOrder;
import org.thingsboard.server.common.data.sync.vc.EntitiesVersionControlSettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.VersionControlAuthMethod; import org.thingsboard.server.common.data.sync.vc.RepositoryAuthMethod;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -77,14 +77,14 @@ public class GitRepository {
private final Git git; private final Git git;
@Getter @Getter
private final EntitiesVersionControlSettings settings; private final RepositorySettings settings;
private final CredentialsProvider credentialsProvider; private final CredentialsProvider credentialsProvider;
private final SshdSessionFactory sshSessionFactory; private final SshdSessionFactory sshSessionFactory;
@Getter @Getter
private final String directory; private final String directory;
private GitRepository(Git git, EntitiesVersionControlSettings settings, CredentialsProvider credentialsProvider, SshdSessionFactory sshSessionFactory, String directory) { private GitRepository(Git git, RepositorySettings settings, CredentialsProvider credentialsProvider, SshdSessionFactory sshSessionFactory, String directory) {
this.git = git; this.git = git;
this.settings = settings; this.settings = settings;
this.credentialsProvider = credentialsProvider; this.credentialsProvider = credentialsProvider;
@ -92,12 +92,12 @@ public class GitRepository {
this.directory = directory; this.directory = directory;
} }
public static GitRepository clone(EntitiesVersionControlSettings settings, File directory) throws GitAPIException { public static GitRepository clone(RepositorySettings settings, File directory) throws GitAPIException {
CredentialsProvider credentialsProvider = null; CredentialsProvider credentialsProvider = null;
SshdSessionFactory sshSessionFactory = null; SshdSessionFactory sshSessionFactory = null;
if (VersionControlAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) { if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) {
credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword()); credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword());
} else if (VersionControlAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) { } else if (RepositoryAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) {
sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory); sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory);
} }
CloneCommand cloneCommand = Git.cloneRepository() CloneCommand cloneCommand = Git.cloneRepository()
@ -109,24 +109,24 @@ public class GitRepository {
return new GitRepository(git, settings, credentialsProvider, sshSessionFactory, directory.getAbsolutePath()); return new GitRepository(git, settings, credentialsProvider, sshSessionFactory, directory.getAbsolutePath());
} }
public static GitRepository open(File directory, EntitiesVersionControlSettings settings) throws IOException { public static GitRepository open(File directory, RepositorySettings settings) throws IOException {
Git git = Git.open(directory); Git git = Git.open(directory);
CredentialsProvider credentialsProvider = null; CredentialsProvider credentialsProvider = null;
SshdSessionFactory sshSessionFactory = null; SshdSessionFactory sshSessionFactory = null;
if (VersionControlAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) { if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) {
credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword()); credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword());
} else if (VersionControlAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) { } else if (RepositoryAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) {
sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory); sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory);
} }
return new GitRepository(git, settings, credentialsProvider, sshSessionFactory, directory.getAbsolutePath()); return new GitRepository(git, settings, credentialsProvider, sshSessionFactory, directory.getAbsolutePath());
} }
public static void test(EntitiesVersionControlSettings settings, File directory) throws GitAPIException { public static void test(RepositorySettings settings, File directory) throws GitAPIException {
CredentialsProvider credentialsProvider = null; CredentialsProvider credentialsProvider = null;
SshdSessionFactory sshSessionFactory = null; SshdSessionFactory sshSessionFactory = null;
if (VersionControlAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) { if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) {
credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword()); credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword());
} else if (VersionControlAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) { } else if (RepositoryAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) {
sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory); sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory);
} }
LsRemoteCommand lsRemoteCommand = Git.lsRemoteRepository().setRemote(settings.getRepositoryUri()); LsRemoteCommand lsRemoteCommand = Git.lsRemoteRepository().setRemote(settings.getRepositoryUri());
@ -255,8 +255,9 @@ public class GitRepository {
return new Status(status.getAdded(), modified, status.getRemoved()); return new Status(status.getAdded(), modified, status.getRemoved());
} }
public Commit commit(String message) throws GitAPIException { public Commit commit(String message, String authorName, String authorEmail) throws GitAPIException {
RevCommit revCommit = execute(git.commit() RevCommit revCommit = execute(git.commit()
.setAuthor(authorName, authorEmail)
.setMessage(message)); .setMessage(message));
return toCommit(revCommit); return toCommit(revCommit);
} }
@ -325,7 +326,8 @@ 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(), revCommit.getAuthorIdent().getEmailAddress());
} }
private RevCommit resolveCommit(String id) throws IOException { private RevCommit resolveCommit(String id) throws IOException {
@ -470,6 +472,7 @@ public class GitRepository {
private final String id; private final String id;
private final String message; private final String message;
private final String authorName; private final String authorName;
private final String authorEmail;
} }
@Data @Data

8
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepositoryService.java

@ -19,7 +19,7 @@ import org.eclipse.jgit.api.errors.GitAPIException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.sync.vc.EntitiesVersionControlSettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.EntityVersion; import org.thingsboard.server.common.data.sync.vc.EntityVersion;
import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo; import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo;
@ -39,11 +39,11 @@ public interface GitRepositoryService {
List<VersionedEntityInfo> listEntitiesAtVersion(TenantId tenantId, String versionId, String path) throws Exception; List<VersionedEntityInfo> listEntitiesAtVersion(TenantId tenantId, String versionId, String path) throws Exception;
void testRepository(TenantId tenantId, EntitiesVersionControlSettings settings) throws Exception; void testRepository(TenantId tenantId, RepositorySettings settings) throws Exception;
void initRepository(TenantId tenantId, EntitiesVersionControlSettings settings) throws Exception; void initRepository(TenantId tenantId, RepositorySettings settings) throws Exception;
EntitiesVersionControlSettings getRepositorySettings(TenantId tenantId) throws Exception; RepositorySettings getRepositorySettings(TenantId tenantId) throws Exception;
void clearRepository(TenantId tenantId) throws IOException; void clearRepository(TenantId tenantId) throws IOException;

8
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/PendingCommit.java

@ -30,12 +30,18 @@ public class PendingCommit {
private String branch; private String branch;
private String versionName; private String versionName;
public PendingCommit(TenantId tenantId, String nodeId, UUID txId, String branch, String versionName) { private String authorName;
private String authorEmail;
public PendingCommit(TenantId tenantId, String nodeId, UUID txId, String branch, String versionName, String authorName, String authorEmail) {
this.tenantId = tenantId; this.tenantId = tenantId;
this.nodeId = nodeId; this.nodeId = nodeId;
this.txId = txId; this.txId = txId;
this.branch = branch; this.branch = branch;
this.versionName = versionName; this.versionName = versionName;
this.authorName = authorName;
this.authorEmail = authorEmail;
this.workingBranch = txId.toString(); this.workingBranch = txId.toString();
} }
} }

6
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlRequestCtx.java

@ -18,7 +18,7 @@ package org.thingsboard.server.service.sync.vc;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.vc.EntitiesVersionControlSettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg;
import java.util.UUID; import java.util.UUID;
@ -29,9 +29,9 @@ public class VersionControlRequestCtx {
private final String nodeId; private final String nodeId;
private final UUID requestId; private final UUID requestId;
private final TenantId tenantId; private final TenantId tenantId;
private final EntitiesVersionControlSettings settings; private final RepositorySettings settings;
public VersionControlRequestCtx(ToVersionControlServiceMsg msg, EntitiesVersionControlSettings settings) { public VersionControlRequestCtx(ToVersionControlServiceMsg msg, RepositorySettings settings) {
this.nodeId = msg.getNodeId(); this.nodeId = msg.getNodeId();
this.requestId = new UUID(msg.getRequestIdMSB(), msg.getRequestIdLSB()); this.requestId = new UUID(msg.getRequestIdMSB(), msg.getRequestIdLSB());
this.tenantId = new TenantId(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); this.tenantId = new TenantId(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB()));

10
ui-ngx/src/app/core/auth/auth.actions.ts

@ -24,7 +24,7 @@ export enum AuthActionTypes {
LOAD_USER = '[Auth] Load User', LOAD_USER = '[Auth] Load User',
UPDATE_USER_DETAILS = '[Auth] Update User Details', UPDATE_USER_DETAILS = '[Auth] Update User Details',
UPDATE_LAST_PUBLIC_DASHBOARD_ID = '[Auth] Update Last Public Dashboard Id', UPDATE_LAST_PUBLIC_DASHBOARD_ID = '[Auth] Update Last Public Dashboard Id',
UPDATE_HAS_VERSION_CONTROL = '[Auth] Change Has Version Control' UPDATE_HAS_REPOSITORY = '[Auth] Change Has Repository'
} }
export class ActionAuthAuthenticated implements Action { export class ActionAuthAuthenticated implements Action {
@ -55,11 +55,11 @@ export class ActionAuthUpdateLastPublicDashboardId implements Action {
constructor(readonly payload: { lastPublicDashboardId: string }) {} constructor(readonly payload: { lastPublicDashboardId: string }) {}
} }
export class ActionAuthUpdateHasVersionControl implements Action { export class ActionAuthUpdateHasRepository implements Action {
readonly type = AuthActionTypes.UPDATE_HAS_VERSION_CONTROL; readonly type = AuthActionTypes.UPDATE_HAS_REPOSITORY;
constructor(readonly payload: { hasVersionControl: boolean }) {} constructor(readonly payload: { hasRepository: boolean }) {}
} }
export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated | export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated |
ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId | ActionAuthUpdateHasVersionControl; ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId | ActionAuthUpdateHasRepository;

2
ui-ngx/src/app/core/auth/auth.models.ts

@ -20,7 +20,7 @@ export interface SysParamsState {
userTokenAccessEnabled: boolean; userTokenAccessEnabled: boolean;
allowedDashboardIds: string[]; allowedDashboardIds: string[];
edgesSupportEnabled: boolean; edgesSupportEnabled: boolean;
hasVersionControl: boolean; hasRepository: boolean;
} }
export interface AuthPayload extends SysParamsState { export interface AuthPayload extends SysParamsState {

4
ui-ngx/src/app/core/auth/auth.reducer.ts

@ -24,7 +24,7 @@ const emptyUserAuthState: AuthPayload = {
forceFullscreen: false, forceFullscreen: false,
allowedDashboardIds: [], allowedDashboardIds: [],
edgesSupportEnabled: false, edgesSupportEnabled: false,
hasVersionControl: false hasRepository: false
}; };
export const initialState: AuthState = { export const initialState: AuthState = {
@ -55,7 +55,7 @@ export function authReducer(
case AuthActionTypes.UPDATE_LAST_PUBLIC_DASHBOARD_ID: case AuthActionTypes.UPDATE_LAST_PUBLIC_DASHBOARD_ID:
return { ...state, ...action.payload}; return { ...state, ...action.payload};
case AuthActionTypes.UPDATE_HAS_VERSION_CONTROL: case AuthActionTypes.UPDATE_HAS_REPOSITORY:
return { ...state, ...action.payload}; return { ...state, ...action.payload};
default: default:

4
ui-ngx/src/app/core/auth/auth.selectors.ts

@ -55,9 +55,9 @@ export const selectUserTokenAccessEnabled = createSelector(
(state: AuthState) => state.userTokenAccessEnabled (state: AuthState) => state.userTokenAccessEnabled
); );
export const selectHasVersionControl = createSelector( export const selectHasRepository = createSelector(
selectAuthState, selectAuthState,
(state: AuthState) => state.hasVersionControl (state: AuthState) => state.hasRepository
); );
export function getCurrentAuthState(store: Store<AppState>): AuthState { export function getCurrentAuthState(store: Store<AppState>): AuthState {

10
ui-ngx/src/app/core/auth/auth.service.ts

@ -437,9 +437,9 @@ export class AuthService {
return this.http.get<boolean>('/api/edges/enabled', defaultHttpOptions()); return this.http.get<boolean>('/api/edges/enabled', defaultHttpOptions());
} }
private loadHasVersionControl(authUser: AuthUser): Observable<boolean> { private loadHasRepository(authUser: AuthUser): Observable<boolean> {
if (authUser.authority === Authority.TENANT_ADMIN) { if (authUser.authority === Authority.TENANT_ADMIN) {
return this.http.get<boolean>('/api/admin/vcSettings/exists', defaultHttpOptions()); return this.http.get<boolean>('/api/admin/repositorySettings/exists', defaultHttpOptions());
} else { } else {
return of(false); return of(false);
} }
@ -449,15 +449,15 @@ export class AuthService {
const sources = [this.loadIsUserTokenAccessEnabled(authPayload.authUser), const sources = [this.loadIsUserTokenAccessEnabled(authPayload.authUser),
this.fetchAllowedDashboardIds(authPayload), this.fetchAllowedDashboardIds(authPayload),
this.loadIsEdgesSupportEnabled(), this.loadIsEdgesSupportEnabled(),
this.loadHasVersionControl(authPayload.authUser), this.loadHasRepository(authPayload.authUser),
this.timeService.loadMaxDatapointsLimit()]; this.timeService.loadMaxDatapointsLimit()];
return forkJoin(sources) return forkJoin(sources)
.pipe(map((data) => { .pipe(map((data) => {
const userTokenAccessEnabled: boolean = data[0] as boolean; const userTokenAccessEnabled: boolean = data[0] as boolean;
const allowedDashboardIds: string[] = data[1] as string[]; const allowedDashboardIds: string[] = data[1] as string[];
const edgesSupportEnabled: boolean = data[2] as boolean; const edgesSupportEnabled: boolean = data[2] as boolean;
const hasVersionControl: boolean = data[3] as boolean; const hasRepository: boolean = data[3] as boolean;
return {userTokenAccessEnabled, allowedDashboardIds, edgesSupportEnabled, hasVersionControl}; return {userTokenAccessEnabled, allowedDashboardIds, edgesSupportEnabled, hasRepository};
}, catchError((err) => { }, catchError((err) => {
return of({}); return of({});
}))); })));

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

@ -15,19 +15,21 @@
/// ///
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs'; import { Observable, of } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { import {
AdminSettings, AdminSettings,
EntitiesVersionControlSettings, RepositorySettings,
MailServerSettings, MailServerSettings,
SecuritySettings, SecuritySettings,
TestSmsRequest, TestSmsRequest,
UpdateMessage UpdateMessage, AutoCommitSettings
} from '@shared/models/settings.models'; } from '@shared/models/settings.models';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service'; import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { AuthUser } from '@shared/models/user.model';
import { Authority } from '@shared/models/authority.enum';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -68,13 +70,13 @@ export class AdminService {
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public getEntitiesVersionControlSettings(config?: RequestConfig): Observable<EntitiesVersionControlSettings> { public getRepositorySettings(config?: RequestConfig): Observable<RepositorySettings> {
return this.http.get<EntitiesVersionControlSettings>(`/api/admin/vcSettings`, defaultHttpOptionsFromConfig(config)); return this.http.get<RepositorySettings>(`/api/admin/repositorySettings`, defaultHttpOptionsFromConfig(config));
} }
public saveEntitiesVersionControlSettings(versionControlSettings: EntitiesVersionControlSettings, public saveRepositorySettings(repositorySettings: RepositorySettings,
config?: RequestConfig): Observable<EntitiesVersionControlSettings> { config?: RequestConfig): Observable<RepositorySettings> {
return this.http.post<EntitiesVersionControlSettings>('/api/admin/vcSettings', versionControlSettings, return this.http.post<RepositorySettings>('/api/admin/repositorySettings', repositorySettings,
defaultHttpOptionsFromConfig(config)).pipe( defaultHttpOptionsFromConfig(config)).pipe(
tap(() => { tap(() => {
this.entitiesVersionControlService.clearBranchList(); this.entitiesVersionControlService.clearBranchList();
@ -82,17 +84,34 @@ export class AdminService {
); );
} }
public deleteEntitiesVersionControlSettings(config?: RequestConfig) { public deleteRepositorySettings(config?: RequestConfig) {
return this.http.delete('/api/admin/vcSettings', defaultHttpOptionsFromConfig(config)).pipe( return this.http.delete('/api/admin/repositorySettings', defaultHttpOptionsFromConfig(config)).pipe(
tap(() => { tap(() => {
this.entitiesVersionControlService.clearBranchList(); this.entitiesVersionControlService.clearBranchList();
}) })
); );
} }
public checkVersionControlAccess(versionControlSettings: EntitiesVersionControlSettings, public checkRepositoryAccess(repositorySettings: RepositorySettings,
config?: RequestConfig): Observable<void> { config?: RequestConfig): Observable<void> {
return this.http.post<void>('/api/admin/vcSettings/checkAccess', versionControlSettings, defaultHttpOptionsFromConfig(config)); return this.http.post<void>('/api/admin/repositorySettings/checkAccess', repositorySettings, defaultHttpOptionsFromConfig(config));
}
public getAutoCommitSettings(config?: RequestConfig): Observable<AutoCommitSettings> {
return this.http.get<AutoCommitSettings>(`/api/admin/autoCommitSettings`, defaultHttpOptionsFromConfig(config));
}
public autoCommitSettingsExists(config?: RequestConfig): Observable<boolean> {
return this.http.get<boolean>('/api/admin/autoCommitSettings/exists', defaultHttpOptionsFromConfig(config));
}
public saveAutoCommitSettings(autoCommitSettings: AutoCommitSettings,
config?: RequestConfig): Observable<AutoCommitSettings> {
return this.http.post<AutoCommitSettings>('/api/admin/autoCommitSettings', autoCommitSettings, defaultHttpOptionsFromConfig(config));
}
public deleteAutoCommitSettings(config?: RequestConfig) {
return this.http.delete('/api/admin/autoCommitSettings', defaultHttpOptionsFromConfig(config));
} }
public checkUpdates(config?: RequestConfig): Observable<UpdateMessage> { public checkUpdates(config?: RequestConfig): Observable<UpdateMessage> {

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

@ -17,7 +17,7 @@
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 { combineLatest, Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { import {
BranchInfo, EntityDataDiff, BranchInfo, EntityDataDiff,
EntityVersion, EntityVersion,
@ -29,10 +29,10 @@ 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 { 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 { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { selectHasVersionControl, selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors'; import { selectIsUserLoaded } from '@core/auth/auth.selectors';
import { catchError, combineAll, tap } from 'rxjs/operators'; import { catchError, tap } from 'rxjs/operators';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'

22
ui-ngx/src/app/core/services/menu.service.ts

@ -369,7 +369,7 @@ export class MenuService {
name: 'admin.system-settings', name: 'admin.system-settings',
type: 'toggle', type: 'toggle',
path: '/settings', path: '/settings',
height: '120px', height: '160px',
icon: 'settings', icon: 'settings',
pages: [ pages: [
{ {
@ -388,10 +388,17 @@ export class MenuService {
}, },
{ {
id: guid(), id: guid(),
name: 'admin.git-settings', name: 'admin.repository-settings',
type: 'link', type: 'link',
path: '/settings/vc', path: '/settings/repository',
icon: 'manage_history' icon: 'manage_history'
},
{
id: guid(),
name: 'admin.auto-commit-settings',
type: 'link',
path: '/settings/auto-commit',
icon: 'settings_backup_restore'
} }
] ]
} }
@ -538,9 +545,14 @@ export class MenuService {
path: '/settings/resources-library' path: '/settings/resources-library'
}, },
{ {
name: 'admin.git-settings', name: 'admin.repository-settings',
icon: 'manage_history', icon: 'manage_history',
path: '/settings/vc', path: '/settings/repository',
},
{
name: 'admin.auto-commit-settings',
icon: 'settings_backup_restore',
path: '/settings/auto-commit'
} }
] ]
} }

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

@ -153,7 +153,7 @@ 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 { VersionControlSettingsComponent } from '@home/components/vc/version-control-settings.component'; import { RepositorySettingsComponent } from '@home/components/vc/repository-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 { EntityVersionCreateComponent } from '@home/components/vc/entity-version-create.component'; import { EntityVersionCreateComponent } from '@home/components/vc/entity-version-create.component';
@ -163,6 +163,8 @@ import { ComplexVersionCreateComponent } from '@home/components/vc/complex-versi
import { EntityTypesVersionCreateComponent } from '@home/components/vc/entity-types-version-create.component'; import { EntityTypesVersionCreateComponent } from '@home/components/vc/entity-types-version-create.component';
import { EntityTypesVersionLoadComponent } from '@home/components/vc/entity-types-version-load.component'; import { EntityTypesVersionLoadComponent } from '@home/components/vc/entity-types-version-load.component';
import { ComplexVersionLoadComponent } from '@home/components/vc/complex-version-load.component'; import { ComplexVersionLoadComponent } from '@home/components/vc/complex-version-load.component';
import { RemoveOtherEntitiesConfirmComponent } from '@home/components/vc/remove-other-entities-confirm.component';
import { AutoCommitSettingsComponent } from '@home/components/vc/auto-commit-settings.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -287,7 +289,7 @@ import { ComplexVersionLoadComponent } from '@home/components/vc/complex-version
DisplayWidgetTypesPanelComponent, DisplayWidgetTypesPanelComponent,
TenantProfileQueuesComponent, TenantProfileQueuesComponent,
QueueFormComponent, QueueFormComponent,
VersionControlSettingsComponent, RepositorySettingsComponent,
VersionControlComponent, VersionControlComponent,
EntityVersionsTableComponent, EntityVersionsTableComponent,
EntityVersionCreateComponent, EntityVersionCreateComponent,
@ -296,7 +298,9 @@ import { ComplexVersionLoadComponent } from '@home/components/vc/complex-version
ComplexVersionCreateComponent, ComplexVersionCreateComponent,
EntityTypesVersionCreateComponent, EntityTypesVersionCreateComponent,
EntityTypesVersionLoadComponent, EntityTypesVersionLoadComponent,
ComplexVersionLoadComponent ComplexVersionLoadComponent,
RemoveOtherEntitiesConfirmComponent,
AutoCommitSettingsComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -415,7 +419,7 @@ import { ComplexVersionLoadComponent } from '@home/components/vc/complex-version
DisplayWidgetTypesPanelComponent, DisplayWidgetTypesPanelComponent,
TenantProfileQueuesComponent, TenantProfileQueuesComponent,
QueueFormComponent, QueueFormComponent,
VersionControlSettingsComponent, RepositorySettingsComponent,
VersionControlComponent, VersionControlComponent,
EntityVersionsTableComponent, EntityVersionsTableComponent,
EntityVersionCreateComponent, EntityVersionCreateComponent,
@ -424,7 +428,9 @@ import { ComplexVersionLoadComponent } from '@home/components/vc/complex-version
ComplexVersionCreateComponent, ComplexVersionCreateComponent,
EntityTypesVersionCreateComponent, EntityTypesVersionCreateComponent,
EntityTypesVersionLoadComponent, EntityTypesVersionLoadComponent,
ComplexVersionLoadComponent ComplexVersionLoadComponent,
RemoveOtherEntitiesConfirmComponent,
AutoCommitSettingsComponent
], ],
providers: [ providers: [
WidgetComponentService, WidgetComponentService,

125
ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html

@ -0,0 +1,125 @@
<!--
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.
-->
<div>
<mat-card class="auto-commit-settings settings-card">
<mat-card-title>
<div fxLayout="row">
<span class="mat-headline" translate>admin.auto-commit-settings</span>
<span fxFlex></span>
<div tb-help="autoCommitSettings"></div>
</div>
</mat-card-title>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<mat-card-content style="padding-top: 16px;">
<form [formGroup]="autoCommitSettingsForm" #formDirective="ngForm" (ngSubmit)="save()">
<fieldset class="fields-group" [disabled]="isLoading$ | async">
<legend class="group-title" translate>admin.auto-commit-entities</legend>
<div fxLayout="column">
<div class="tb-control-list">
<div *ngFor="let entityTypeFormGroup of entityTypesFormGroupArray(); trackBy: trackByEntityType;
let $index = index; last as isLast;"
fxLayout="row" fxLayoutAlign="start center" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
<mat-expansion-panel class="entity-type-config" fxFlex [formGroup]="entityTypeFormGroup" [expanded]="entityTypesFormGroupExpanded(entityTypeFormGroup)">
<mat-expansion-panel-header>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<mat-panel-title>
<div fxLayout="row" fxFlex fxLayoutAlign="start center">
<div [innerHTML]="entityTypeText(entityTypeFormGroup)"></div>
</div>
</mat-panel-title>
<span fxFlex></span>
<button mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeEntityType($index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="entity-type-config-content" fxLayout="column" fxLayoutGap="0.5em">
<mat-divider></mat-divider>
<div fxLayout="column" fxLayout.gt-lg="row" fxLayoutGap.gt-lg="16px">
<div fxLayout="row" fxLayoutGap="16px">
<tb-entity-type-select
showLabel
formControlName="entityType"
required
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)">
</tb-entity-type-select>
<div formGroupName="config">
<tb-branch-autocomplete
emptyPlaceholder="{{ 'version-control.default' | translate }}"
[selectDefaultBranch]="false"
formControlName="branch">
</tb-branch-autocomplete>
</div>
</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px" formGroupName="config">
<mat-checkbox formControlName="saveRelations">
{{ 'version-control.export-entity-relations' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="saveAttributes">
{{ 'version-control.export-entity-attributes' | translate }}
</mat-checkbox>
</div>
</div>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</div>
<div *ngIf="!entityTypesFormGroupArray().length">
<span translate fxLayoutAlign="center center"
class="tb-prompt">admin.no-auto-commit-entities-prompt</span>
</div>
<div style="padding-top: 16px;" fxLayout="row">
<button mat-raised-button color="primary"
type="button"
[disabled]="!addEnabled()"
(click)="addEntityType()">
<span translate>version-control.add-entity-type</span>
</button>
<span fxFlex></span>
<button mat-raised-button color="primary"
type="button"
[disabled]="!entityTypesFormGroupArray().length"
(click)="removeAll()">
<span translate>version-control.remove-all</span>
</button>
</div>
</div>
</fieldset>
<div fxLayout="row" fxLayoutAlign="end center" fxLayout.xs="column" fxLayoutAlign.xs="end" fxLayoutGap="16px">
<button mat-raised-button color="warn" type="button" [fxShow]="settings !== null"
[disabled]="(isLoading$ | async)" (click)="delete(formDirective)">
{{'action.delete' | translate}}
</button>
<span fxFlex></span>
<button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || autoCommitSettingsForm.invalid || !autoCommitSettingsForm.dirty"
type="submit">{{'action.save' | translate}}
</button>
</div>
</form>
</mat-card-content>
</mat-card>
</div>

74
ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.scss

@ -0,0 +1,74 @@
/**
* 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 {
mat-card.auto-commit-settings {
margin: 8px;
.mat-divider {
position: relative;
}
}
.fields-group {
padding: 0 16px 8px;
margin-bottom: 10px;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
legend {
color: rgba(0, 0, 0, .7);
width: fit-content;
}
legend + * {
display: block;
margin-top: 16px;
}
}
.tb-control-list {
overflow-y: auto;
max-height: 600px;
}
.tb-prompt {
margin: 30px 0;
}
mat-expansion-panel.entity-type-config {
box-shadow: none;
border: 1px groove rgba(0, 0, 0, .25);
.mat-expansion-panel-header {
padding: 0 24px 0 8px;
height: 48px;
}
.entity-type-config-content {
padding: 0 8px 8px;
tb-branch-autocomplete {
min-width: 200px;
max-width: 200px;
display: block;
}
}
}
}
:host ::ng-deep {
.mat-expansion-panel.entity-type-config {
.mat-expansion-panel-body {
padding: 0;
}
}
}

210
ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.ts

@ -0,0 +1,210 @@
///
/// 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, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { AbstractControl, FormArray, FormBuilder, FormGroup, FormGroupDirective, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AdminService } from '@core/http/admin.service';
import { AutoCommitSettings, AutoVersionCreateConfig } from '@shared/models/settings.models';
import { TranslateService } from '@ngx-translate/core';
import { DialogService } from '@core/services/dialog.service';
import { catchError, mergeMap } from 'rxjs/operators';
import { of } from 'rxjs';
import { EntityTypeVersionCreateConfig, exportableEntityTypes } from '@shared/models/vc.models';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Component({
selector: 'tb-auto-commit-settings',
templateUrl: './auto-commit-settings.component.html',
styleUrls: ['./auto-commit-settings.component.scss', './../../pages/admin/settings-card.scss']
})
export class AutoCommitSettingsComponent extends PageComponent implements OnInit {
autoCommitSettingsForm: FormGroup;
settings: AutoCommitSettings = null;
constructor(protected store: Store<AppState>,
private adminService: AdminService,
private dialogService: DialogService,
private sanitizer: DomSanitizer,
private translate: TranslateService,
public fb: FormBuilder) {
super(store);
}
ngOnInit() {
this.autoCommitSettingsForm = this.fb.group({
entityTypes: this.fb.array([], [])
});
this.adminService.autoCommitSettingsExists().pipe(
catchError(() => of(false)),
mergeMap((hasAutoCommitSettings) => {
if (hasAutoCommitSettings) {
return this.adminService.getAutoCommitSettings({ignoreErrors: true}).pipe(
catchError(() => of(null))
);
} else {
return of(null);
}
})
).subscribe(
(settings) => {
this.settings = settings;
this.autoCommitSettingsForm.setControl('entityTypes',
this.prepareEntityTypesFormArray(settings), {emitEvent: false});
});
}
entityTypesFormGroupArray(): FormGroup[] {
return (this.autoCommitSettingsForm.get('entityTypes') as FormArray).controls as FormGroup[];
}
entityTypesFormGroupExpanded(entityTypeControl: AbstractControl): boolean {
return !!(entityTypeControl as any).expanded;
}
public trackByEntityType(index: number, entityTypeControl: AbstractControl): any {
return entityTypeControl;
}
public removeEntityType(index: number) {
(this.autoCommitSettingsForm.get('entityTypes') as FormArray).removeAt(index);
this.autoCommitSettingsForm.markAsDirty();
}
public addEnabled(): boolean {
const entityTypesArray = this.autoCommitSettingsForm.get('entityTypes') as FormArray;
return entityTypesArray.length < exportableEntityTypes.length;
}
public addEntityType() {
const entityTypesArray = this.autoCommitSettingsForm.get('entityTypes') as FormArray;
const config: AutoVersionCreateConfig = {
branch: null,
saveRelations: false,
saveAttributes: false
};
const allowed = this.allowedEntityTypes();
let entityType: EntityType = null;
if (allowed.length) {
entityType = allowed[0];
}
const entityTypeControl = this.createEntityTypeControl(entityType, config);
(entityTypeControl as any).expanded = true;
entityTypesArray.push(entityTypeControl);
this.autoCommitSettingsForm.updateValueAndValidity();
this.autoCommitSettingsForm.markAsDirty();
}
public removeAll() {
const entityTypesArray = this.autoCommitSettingsForm.get('entityTypes') as FormArray;
entityTypesArray.clear();
this.autoCommitSettingsForm.updateValueAndValidity();
this.autoCommitSettingsForm.markAsDirty();
}
entityTypeText(entityTypeControl: AbstractControl): SafeHtml {
const entityType: EntityType = entityTypeControl.get('entityType').value;
const config: AutoVersionCreateConfig = entityTypeControl.get('config').value;
let message = entityType ? this.translate.instant(entityTypeTranslations.get(entityType).typePlural) : 'Undefined';
let branchName;
if (config.branch) {
branchName = config.branch;
} else {
branchName = this.translate.instant('version-control.default');
}
message += ` (<small>${this.translate.instant('version-control.auto-commit-to-branch', {branch: branchName})}</small>)`;
return this.sanitizer.bypassSecurityTrustHtml(message);
}
allowedEntityTypes(entityTypeControl?: AbstractControl): Array<EntityType> {
let res = [...exportableEntityTypes];
const currentEntityType: EntityType = entityTypeControl?.get('entityType')?.value;
const value: [{entityType: string, config: EntityTypeVersionCreateConfig}] =
this.autoCommitSettingsForm.get('entityTypes').value || [];
const usedEntityTypes = value.map(val => val.entityType).filter(val => val);
res = res.filter(entityType => !usedEntityTypes.includes(entityType) || entityType === currentEntityType);
return res;
}
save(): void {
const value: [{entityType: string, config: AutoVersionCreateConfig}] =
this.autoCommitSettingsForm.get('entityTypes').value || [];
const settings: AutoCommitSettings = {};
if (value && value.length) {
value.forEach((val) => {
settings[val.entityType] = val.config;
});
}
this.adminService.saveAutoCommitSettings(settings).subscribe(
(savedSettings) => {
this.settings = savedSettings;
this.autoCommitSettingsForm.setControl('entityTypes',
this.prepareEntityTypesFormArray(savedSettings), {emitEvent: false});
this.autoCommitSettingsForm.markAsPristine();
}
);
}
delete(formDirective: FormGroupDirective): void {
this.dialogService.confirm(
this.translate.instant('admin.delete-auto-commit-settings-title', ),
this.translate.instant('admin.delete-auto-commit-settings-text'), null,
this.translate.instant('action.delete')
).subscribe((data) => {
if (data) {
this.adminService.deleteAutoCommitSettings().subscribe(
() => {
this.settings = null;
this.autoCommitSettingsForm.setControl('entityTypes',
this.prepareEntityTypesFormArray(this.settings), {emitEvent: false});
this.autoCommitSettingsForm.markAsPristine();
}
);
}
});
}
private prepareEntityTypesFormArray(settings: AutoCommitSettings | null): FormArray {
const entityTypesControls: Array<AbstractControl> = [];
if (settings) {
for (const entityType of Object.keys(settings)) {
const config = settings[entityType];
entityTypesControls.push(this.createEntityTypeControl(entityType as EntityType, config));
}
}
return this.fb.array(entityTypesControls);
}
private createEntityTypeControl(entityType: EntityType, config: AutoVersionCreateConfig): AbstractControl {
const entityTypeControl = this.fb.group(
{
entityType: [entityType, [Validators.required]],
config: this.fb.group({
branch: [config.branch, []],
saveRelations: [config.saveRelations, []],
saveAttributes: [config.saveAttributes, []]
})
}
);
return entityTypeControl;
}
}

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

@ -17,7 +17,7 @@
--> -->
<section class="entity-types-version-create" [formGroup]="entityTypesVersionCreateFormGroup" fxLayout="column"> <section class="entity-types-version-create" [formGroup]="entityTypesVersionCreateFormGroup" fxLayout="column">
<fieldset class="fields-group"> <fieldset class="fields-group">
<legend class="group-title" translate>version-control.entity-types</legend> <legend class="group-title" translate>version-control.entities-to-export</legend>
<div fxLayout="column"> <div fxLayout="column">
<div class="tb-control-list"> <div class="tb-control-list">
<div *ngFor="let entityTypeFormGroup of entityTypesFormGroupArray(); trackBy: trackByEntityType; <div *ngFor="let entityTypeFormGroup of entityTypesFormGroupArray(); trackBy: trackByEntityType;
@ -53,7 +53,7 @@
[allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)"> [allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)">
</tb-entity-type-select> </tb-entity-type-select>
<div fxFlex fxLayout="row" fxLayoutGap="16px" fxLayoutAlign="start center" formGroupName="config"> <div fxFlex fxLayout="row" fxLayoutGap="16px" fxLayoutAlign="start center" formGroupName="config">
<mat-form-field class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>version-control.sync-strategy</mat-label> <mat-label translate>version-control.sync-strategy</mat-label>
<mat-select formControlName="syncStrategy"> <mat-select formControlName="syncStrategy">
<mat-option [value]="'default'"> <mat-option [value]="'default'">
@ -64,9 +64,14 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-checkbox formControlName="saveRelations"> <div fxFlex fxLayout="column" fxLayoutGap="8px">
{{ 'version-control.export-entity-relations' | translate }} <mat-checkbox formControlName="saveRelations">
</mat-checkbox> {{ 'version-control.export-entity-relations' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="saveAttributes">
{{ 'version-control.export-entity-attributes' | translate }}
</mat-checkbox>
</div>
</div> </div>
</div> </div>
<div fxLayout="row" fxLayoutGap="16px" fxLayoutAlign="start center" <div fxLayout="row" fxLayoutGap="16px" fxLayoutAlign="start center"
@ -89,7 +94,7 @@
</div> </div>
<div *ngIf="!entityTypesFormGroupArray().length"> <div *ngIf="!entityTypesFormGroupArray().length">
<span translate fxLayoutAlign="center center" <span translate fxLayoutAlign="center center"
class="tb-prompt">version-control.no-entity-types</span> class="tb-prompt">version-control.no-entities-to-export-prompt</span>
</div> </div>
<div style="padding-top: 16px;" fxLayout="row"> <div style="padding-top: 16px;" fxLayout="row">
<button mat-raised-button color="primary" <button mat-raised-button color="primary"

4
ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts

@ -80,7 +80,7 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements
ngOnInit(): void { ngOnInit(): void {
this.entityTypesVersionCreateFormGroup = this.fb.group({ this.entityTypesVersionCreateFormGroup = this.fb.group({
entityTypes: [this.fb.array([]), []] entityTypes: this.fb.array([], [])
}); });
this.entityTypesVersionCreateFormGroup.valueChanges.subscribe(() => { this.entityTypesVersionCreateFormGroup.valueChanges.subscribe(() => {
this.updateModel(); this.updateModel();
@ -135,6 +135,7 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements
config: this.fb.group({ config: this.fb.group({
syncStrategy: [config.syncStrategy === null ? 'default' : config.syncStrategy, []], syncStrategy: [config.syncStrategy === null ? 'default' : config.syncStrategy, []],
saveRelations: [config.saveRelations, []], saveRelations: [config.saveRelations, []],
saveAttributes: [config.saveAttributes, []],
allEntities: [config.allEntities, []], allEntities: [config.allEntities, []],
entityIds: [config.entityIds, [Validators.required]] entityIds: [config.entityIds, [Validators.required]]
}) })
@ -183,6 +184,7 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements
const config: EntityTypeVersionCreateConfig = { const config: EntityTypeVersionCreateConfig = {
syncStrategy: null, syncStrategy: null,
saveRelations: false, saveRelations: false,
saveAttributes: false,
allEntities: true, allEntities: true,
entityIds: [] entityIds: []
}; };

32
ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html

@ -17,7 +17,7 @@
--> -->
<section class="entity-types-version-load" [formGroup]="entityTypesVersionLoadFormGroup" fxLayout="column"> <section class="entity-types-version-load" [formGroup]="entityTypesVersionLoadFormGroup" fxLayout="column">
<fieldset class="fields-group"> <fieldset class="fields-group">
<legend class="group-title" translate>version-control.entity-types</legend> <legend class="group-title" translate>version-control.entities-to-restore</legend>
<div fxLayout="column"> <div fxLayout="column">
<div class="tb-control-list"> <div class="tb-control-list">
<div *ngFor="let entityTypeFormGroup of entityTypesFormGroupArray(); trackBy: trackByEntityType; <div *ngFor="let entityTypeFormGroup of entityTypesFormGroupArray(); trackBy: trackByEntityType;
@ -44,7 +44,7 @@
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<div class="entity-type-config-content" fxLayout="column" fxLayoutGap="0.5em"> <div class="entity-type-config-content" fxLayout="column" fxLayoutGap="0.5em">
<mat-divider></mat-divider> <mat-divider></mat-divider>
<div fxLayout="row" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutGap="32px">
<tb-entity-type-select <tb-entity-type-select
showLabel showLabel
formControlName="entityType" formControlName="entityType"
@ -52,13 +52,25 @@
[filterAllowedEntityTypes]="false" [filterAllowedEntityTypes]="false"
[allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)"> [allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)">
</tb-entity-type-select> </tb-entity-type-select>
<div fxFlex fxLayout="row" fxLayoutGap="16px" fxLayoutAlign="start center" formGroupName="config"> <div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px" formGroupName="config">
<mat-checkbox formControlName="removeOtherEntities"> <div fxFlex fxLayout="column" fxLayoutGap="8px">
{{ 'version-control.remove-other-entities' | translate }} <mat-checkbox #removeOtherEntitiesCheckbox
</mat-checkbox> formControlName="removeOtherEntities"
<mat-checkbox formControlName="loadRelations"> (click)="onRemoveOtherEntities(removeOtherEntitiesCheckbox, entityTypeFormGroup, $event)">
{{ 'version-control.load-entities-relations' | translate }} {{ 'version-control.remove-other-entities' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox formControlName="findExistingEntityByName">
{{ 'version-control.find-existing-entity-by-name' | translate }}
</mat-checkbox>
</div>
<div fxFlex fxLayout="column" fxLayoutGap="8px">
<mat-checkbox formControlName="loadRelations">
{{ 'version-control.load-entities-relations' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="loadAttributes">
{{ 'version-control.load-entities-attributes' | translate }}
</mat-checkbox>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -68,7 +80,7 @@
</div> </div>
<div *ngIf="!entityTypesFormGroupArray().length"> <div *ngIf="!entityTypesFormGroupArray().length">
<span translate fxLayoutAlign="center center" <span translate fxLayoutAlign="center center"
class="tb-prompt">version-control.no-entity-types</span> class="tb-prompt">version-control.no-entities-to-restore-prompt</span>
</div> </div>
<div style="padding-top: 16px;" fxLayout="row"> <div style="padding-top: 16px;" fxLayout="row">
<button mat-raised-button color="primary" <button mat-raised-button color="primary"

44
ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import { import {
AbstractControl, AbstractControl,
ControlValueAccessor, ControlValueAccessor,
@ -28,11 +28,15 @@ import {
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { EntityTypeVersionLoadConfig, exportableEntityTypes } from '@shared/models/vc.models'; import { EntityTypeVersionLoadConfig, exportableEntityTypes, VersionCreationResult } from '@shared/models/vc.models';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { MatCheckbox } from '@angular/material/checkbox/checkbox';
import { TbPopoverService } from '@shared/components/popover.service';
import { EntityVersionCreateComponent } from '@home/components/vc/entity-version-create.component';
import { RemoveOtherEntitiesConfirmComponent } from '@home/components/vc/remove-other-entities-confirm.component';
@Component({ @Component({
selector: 'tb-entity-types-version-load', selector: 'tb-entity-types-version-load',
@ -64,13 +68,16 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private translate: TranslateService, private translate: TranslateService,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private fb: FormBuilder) { private fb: FormBuilder) {
super(store); super(store);
} }
ngOnInit(): void { ngOnInit(): void {
this.entityTypesVersionLoadFormGroup = this.fb.group({ this.entityTypesVersionLoadFormGroup = this.fb.group({
entityTypes: [this.fb.array([]), []] entityTypes: this.fb.array([], [])
}); });
this.entityTypesVersionLoadFormGroup.valueChanges.subscribe(() => { this.entityTypesVersionLoadFormGroup.valueChanges.subscribe(() => {
this.updateModel(); this.updateModel();
@ -124,7 +131,9 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On
entityType: [entityType, [Validators.required]], entityType: [entityType, [Validators.required]],
config: this.fb.group({ config: this.fb.group({
loadRelations: [config.loadRelations, []], loadRelations: [config.loadRelations, []],
removeOtherEntities: [config.removeOtherEntities, []] loadAttributes: [config.loadAttributes, []],
removeOtherEntities: [config.removeOtherEntities, []],
findExistingEntityByName: [config.findExistingEntityByName, []]
}) })
} }
); );
@ -156,7 +165,9 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On
const entityTypesArray = this.entityTypesVersionLoadFormGroup.get('entityTypes') as FormArray; const entityTypesArray = this.entityTypesVersionLoadFormGroup.get('entityTypes') as FormArray;
const config: EntityTypeVersionLoadConfig = { const config: EntityTypeVersionLoadConfig = {
loadRelations: false, loadRelations: false,
removeOtherEntities: false loadAttributes: false,
removeOtherEntities: false,
findExistingEntityByName: false
}; };
const allowed = this.allowedEntityTypes(); const allowed = this.allowedEntityTypes();
let entityType: EntityType = null; let entityType: EntityType = null;
@ -194,6 +205,29 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On
return res; return res;
} }
onRemoveOtherEntities(removeOtherEntitiesCheckbox: MatCheckbox, entityTypeControl: AbstractControl, $event: Event) {
const removeOtherEntities: boolean = entityTypeControl.get('config.removeOtherEntities').value;
if (!removeOtherEntities) {
$event.preventDefault();
$event.stopPropagation();
const trigger = $('.mat-checkbox-frame', removeOtherEntitiesCheckbox._elementRef.nativeElement)[0];
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const removeOtherEntitiesConfirmPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, RemoveOtherEntitiesConfirmComponent, 'bottom', true, null,
{
onClose: (result: boolean | null) => {
removeOtherEntitiesConfirmPopover.hide();
if (result) {
entityTypeControl.get('config').get('removeOtherEntities').patchValue(true, {emitEvent: true});
}
}
}, {}, {}, {}, false);
}
}
}
private updateModel() { private updateModel() {
const value: [{entityType: string, config: EntityTypeVersionLoadConfig}] = const value: [{entityType: string, config: EntityTypeVersionLoadConfig}] =
this.entityTypesVersionLoadFormGroup.get('entityTypes').value || []; this.entityTypesVersionLoadFormGroup.get('entityTypes').value || [];

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

@ -41,6 +41,9 @@
<mat-checkbox formControlName="saveRelations" style="margin-bottom: 16px;"> <mat-checkbox formControlName="saveRelations" style="margin-bottom: 16px;">
{{ 'version-control.export-entity-relations' | translate }} {{ 'version-control.export-entity-relations' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox formControlName="saveAttributes" style="margin-bottom: 16px;">
{{ 'version-control.export-entity-attributes' | translate }}
</mat-checkbox>
</div> </div>
</fieldset> </fieldset>
</form> </form>

6
ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts

@ -62,7 +62,8 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni
this.createVersionFormGroup = this.fb.group({ this.createVersionFormGroup = this.fb.group({
branch: [this.branch, [Validators.required]], branch: [this.branch, [Validators.required]],
versionName: [null, [Validators.required]], versionName: [null, [Validators.required]],
saveRelations: [false, []] saveRelations: [false, []],
saveAttributes: [false, []]
}); });
} }
@ -78,7 +79,8 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni
branch: this.createVersionFormGroup.get('branch').value, branch: this.createVersionFormGroup.get('branch').value,
versionName: this.createVersionFormGroup.get('versionName').value, versionName: this.createVersionFormGroup.get('versionName').value,
config: { config: {
saveRelations: this.createVersionFormGroup.get('saveRelations').value saveRelations: this.createVersionFormGroup.get('saveRelations').value,
saveAttributes: this.createVersionFormGroup.get('saveAttributes').value,
}, },
type: VersionCreateRequestType.SINGLE_ENTITY type: VersionCreateRequestType.SINGLE_ENTITY
}; };

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

@ -29,6 +29,9 @@
<mat-checkbox formControlName="loadRelations" style="margin-bottom: 16px;"> <mat-checkbox formControlName="loadRelations" style="margin-bottom: 16px;">
{{ 'version-control.load-entity-relations' | translate }} {{ 'version-control.load-entity-relations' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox formControlName="loadAttributes" style="margin-bottom: 16px;">
{{ 'version-control.load-entity-attributes' | translate }}
</mat-checkbox>
</div> </div>
</fieldset> </fieldset>
</form> </form>

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

@ -57,7 +57,8 @@ export class EntityVersionRestoreComponent extends PageComponent implements OnIn
ngOnInit(): void { ngOnInit(): void {
this.restoreFormGroup = this.fb.group({ this.restoreFormGroup = this.fb.group({
loadRelations: [false, []] loadRelations: [false, []],
loadAttributes: [false, []]
}); });
} }
@ -73,7 +74,8 @@ export class EntityVersionRestoreComponent extends PageComponent implements OnIn
versionId: this.versionId, versionId: this.versionId,
externalEntityId: this.externalEntityId, externalEntityId: this.externalEntityId,
config: { config: {
loadRelations: this.restoreFormGroup.get('loadRelations').value loadRelations: this.restoreFormGroup.get('loadRelations').value,
loadAttributes: this.restoreFormGroup.get('loadAttributes').value
}, },
type: VersionLoadRequestType.SINGLE_ENTITY type: VersionLoadRequestType.SINGLE_ENTITY
}; };

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

@ -101,11 +101,17 @@
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef style="width: 100%"> {{ 'version-control.version-name' | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef style="width: 50%"> {{ '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="author">
<mat-header-cell *matHeaderCellDef style="width: 50%"> {{ 'version-control.author' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let entityVersion">
{{ entityVersion.author }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef [ngStyle]="singleEntityMode ? {minWidth: '80px', maxWidth: '80px', width: '80px'} : <mat-header-cell *matHeaderCellDef [ngStyle]="singleEntityMode ? {minWidth: '80px', maxWidth: '80px', width: '80px'} :
{minWidth: '40px', maxWidth: '40px', width: '40px'}"> {minWidth: '40px', maxWidth: '40px', width: '40px'}">

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

@ -62,7 +62,7 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
@Input() @Input()
singleEntityMode = false; singleEntityMode = false;
displayedColumns = ['timestamp', 'id', 'name', 'actions']; displayedColumns = ['timestamp', 'id', 'name', 'author', 'actions'];
pageLink: PageLink; pageLink: PageLink;
textSearchMode = false; textSearchMode = false;
dataSource: EntityVersionsDatasource; dataSource: EntityVersionsDatasource;

41
ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html

@ -0,0 +1,41 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section style="min-width: 300px;">
<form [formGroup]="confirmFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="(isLoading$ | async)">
<div style="user-select: none;" [innerHTML]="removeOtherEntitiesConfirmText"></div>
<mat-form-field class="mat-block">
<input matInput formControlName="verification"/>
</mat-form-field>
</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)="confirm()"
[disabled]="(isLoading$ | async) || confirmFormGroup.invalid || confirmFormGroup.get('verification').value !== removeOtherEntitiesVerificationText">
{{ 'action.confirm' | translate }}
</button>
</div>
</section>

66
ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.ts

@ -0,0 +1,66 @@
///
/// 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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Component({
selector: 'tb-remove-other-entities-confirm',
templateUrl: './remove-other-entities-confirm.component.html',
styleUrls: []
})
export class RemoveOtherEntitiesConfirmComponent extends PageComponent implements OnInit {
@Input()
onClose: (result: boolean | null) => void;
confirmFormGroup: FormGroup;
removeOtherEntitiesConfirmText: SafeHtml;
removeOtherEntitiesVerificationText = 'remove other entities';
constructor(protected store: Store<AppState>,
private translate: TranslateService,
private sanitizer: DomSanitizer,
private fb: FormBuilder) {
super(store);
this.removeOtherEntitiesConfirmText = this.sanitizer.bypassSecurityTrustHtml(this.translate.instant('version-control.remove-other-entities-confirm-text'));
}
ngOnInit(): void {
this.confirmFormGroup = this.fb.group({
verification: [null, []]
});
}
cancel(): void {
if (this.onClose) {
this.onClose(null);
}
}
confirm(): void {
if (this.onClose) {
this.onClose(true);
}
}
}

26
ui-ngx/src/app/modules/home/components/vc/version-control-settings.component.html → ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html

@ -16,24 +16,24 @@
--> -->
<div> <div>
<mat-card class="vc-settings" [ngClass]="{'settings-card': !detailsMode}"> <mat-card class="repository-settings" [ngClass]="{'settings-card': !detailsMode}">
<mat-card-title> <mat-card-title>
<div fxLayout="row"> <div fxLayout="row">
<span class="mat-headline" translate>admin.git-repository-settings</span> <span class="mat-headline" translate>admin.repository-settings</span>
<span fxFlex></span> <span fxFlex></span>
<div tb-help="versionControlSettings"></div> <div tb-help="repositorySettings"></div>
</div> </div>
</mat-card-title> </mat-card-title>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar> </mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<mat-card-content style="padding-top: 16px;"> <mat-card-content style="padding-top: 16px;">
<form [formGroup]="versionControlSettingsForm" #formDirective="ngForm" (ngSubmit)="save()"> <form [formGroup]="repositorySettingsForm" #formDirective="ngForm" (ngSubmit)="save()">
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>admin.repository-url</mat-label> <mat-label translate>admin.repository-url</mat-label>
<input matInput required formControlName="repositoryUri"> <input matInput required formControlName="repositoryUri">
<mat-error translate *ngIf="versionControlSettingsForm.get('repositoryUri').hasError('required')"> <mat-error translate *ngIf="repositorySettingsForm.get('repositoryUri').hasError('required')">
admin.repository-url-required admin.repository-url-required
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -46,12 +46,12 @@
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.auth-method</mat-label> <mat-label translate>admin.auth-method</mat-label>
<mat-select required formControlName="authMethod"> <mat-select required formControlName="authMethod">
<mat-option *ngFor="let method of versionControlAuthMethods" [value]="method"> <mat-option *ngFor="let method of repositoryAuthMethods" [value]="method">
{{versionControlAuthMethodTranslations.get(method) | translate}} {{repositoryAuthMethodTranslations.get(method) | translate}}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<section [fxShow]="versionControlSettingsForm.get('authMethod').value === versionControlAuthMethod.USERNAME_PASSWORD" fxLayout="column"> <section [fxShow]="repositorySettingsForm.get('authMethod').value === repositoryAuthMethod.USERNAME_PASSWORD" fxLayout="column">
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>common.username</mat-label> <mat-label translate>common.username</mat-label>
<input matInput formControlName="username" placeholder="{{ 'common.enter-username' | translate }}" <input matInput formControlName="username" placeholder="{{ 'common.enter-username' | translate }}"
@ -68,14 +68,14 @@
<tb-toggle-password matSuffix></tb-toggle-password> <tb-toggle-password matSuffix></tb-toggle-password>
</mat-form-field> </mat-form-field>
</section> </section>
<section [fxShow]="versionControlSettingsForm.get('authMethod').value === versionControlAuthMethod.PRIVATE_KEY" fxLayout="column"> <section [fxShow]="repositorySettingsForm.get('authMethod').value === repositoryAuthMethod.PRIVATE_KEY" fxLayout="column">
<tb-file-input style="margin-bottom: 16px;" <tb-file-input style="margin-bottom: 16px;"
[existingFileName]="versionControlSettingsForm.get('privateKeyFileName').value" [existingFileName]="repositorySettingsForm.get('privateKeyFileName').value"
required required
formControlName="privateKey" formControlName="privateKey"
dropLabel="{{ 'admin.drop-private-key-file-or' | translate }}" dropLabel="{{ 'admin.drop-private-key-file-or' | translate }}"
[label]="'admin.private-key' | translate" [label]="'admin.private-key' | translate"
(fileNameChanged)="versionControlSettingsForm.get('privateKeyFileName').patchValue($event)"> (fileNameChanged)="repositorySettingsForm.get('privateKeyFileName').patchValue($event)">
</tb-file-input> </tb-file-input>
<mat-checkbox *ngIf="showChangePrivateKeyPassword" (change)="changePrivateKeyPasswordChanged()" <mat-checkbox *ngIf="showChangePrivateKeyPassword" (change)="changePrivateKeyPasswordChanged()"
[(ngModel)]="changePrivateKeyPassword" [ngModelOptions]="{ standalone: true }" style="padding-bottom: 16px;"> [(ngModel)]="changePrivateKeyPassword" [ngModelOptions]="{ standalone: true }" style="padding-bottom: 16px;">
@ -96,10 +96,10 @@
</button> </button>
<span fxFlex></span> <span fxFlex></span>
<button mat-raised-button type="button" <button mat-raised-button type="button"
[disabled]="(isLoading$ | async) || versionControlSettingsForm.invalid" (click)="checkAccess()"> [disabled]="(isLoading$ | async) || repositorySettingsForm.invalid" (click)="checkAccess()">
{{'admin.check-access' | translate}} {{'admin.check-access' | translate}}
</button> </button>
<button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || versionControlSettingsForm.invalid || !versionControlSettingsForm.dirty" <button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || repositorySettingsForm.invalid || !repositorySettingsForm.dirty"
type="submit">{{'action.save' | translate}} type="submit">{{'action.save' | translate}}
</button> </button>
</div> </div>

2
ui-ngx/src/app/modules/home/components/vc/version-control-settings.component.scss → ui-ngx/src/app/modules/home/components/vc/repository-settings.component.scss

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
mat-card.vc-settings { mat-card.repository-settings {
margin: 8px; margin: 8px;
} }
.fields-group { .fields-group {

216
ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts

@ -0,0 +1,216 @@
///
/// 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, FormGroupDirective, Validators } from '@angular/forms';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AdminService } from '@core/http/admin.service';
import {
RepositorySettings,
RepositoryAuthMethod,
repositoryAuthMethodTranslationMap
} from '@shared/models/settings.models';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { isNotEmptyStr } from '@core/utils';
import { DialogService } from '@core/services/dialog.service';
import { ActionAuthUpdateHasRepository } from '@core/auth/auth.actions';
import { selectHasRepository } from '@core/auth/auth.selectors';
import { catchError, mergeMap, take } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({
selector: 'tb-repository-settings',
templateUrl: './repository-settings.component.html',
styleUrls: ['./repository-settings.component.scss', './../../pages/admin/settings-card.scss']
})
export class RepositorySettingsComponent extends PageComponent implements OnInit {
@Input()
detailsMode = false;
repositorySettingsForm: FormGroup;
settings: RepositorySettings = null;
repositoryAuthMethod = RepositoryAuthMethod;
repositoryAuthMethods = Object.values(RepositoryAuthMethod);
repositoryAuthMethodTranslations = repositoryAuthMethodTranslationMap;
showChangePassword = false;
changePassword = false;
showChangePrivateKeyPassword = false;
changePrivateKeyPassword = false;
constructor(protected store: Store<AppState>,
private adminService: AdminService,
private dialogService: DialogService,
private translate: TranslateService,
public fb: FormBuilder) {
super(store);
}
ngOnInit() {
this.repositorySettingsForm = this.fb.group({
repositoryUri: [null, [Validators.required]],
defaultBranch: ['main', []],
authMethod: [RepositoryAuthMethod.USERNAME_PASSWORD, [Validators.required]],
username: [null, []],
password: [null, []],
privateKeyFileName: [null, [Validators.required]],
privateKey: [null, []],
privateKeyPassword: [null, []]
});
this.updateValidators(false);
this.repositorySettingsForm.get('authMethod').valueChanges.subscribe(() => {
this.updateValidators(true);
});
this.repositorySettingsForm.get('privateKeyFileName').valueChanges.subscribe(() => {
this.updateValidators(false);
});
this.store.pipe(
select(selectHasRepository),
take(1),
mergeMap((hasRepository) => {
if (hasRepository) {
return this.adminService.getRepositorySettings({ignoreErrors: true}).pipe(
catchError(() => of(null))
);
} else {
return of(null);
}
})
).subscribe(
(settings) => {
this.settings = settings;
if (this.settings != null) {
if (this.settings.authMethod === RepositoryAuthMethod.USERNAME_PASSWORD) {
this.showChangePassword = true;
} else {
this.showChangePrivateKeyPassword = true;
}
this.repositorySettingsForm.reset(this.settings);
this.updateValidators(false);
}
});
}
checkAccess(): void {
const settings: RepositorySettings = this.repositorySettingsForm.value;
this.adminService.checkRepositoryAccess(settings).subscribe(() => {
this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.check-repository-access-success'),
type: 'success' }));
});
}
save(): void {
const settings: RepositorySettings = this.repositorySettingsForm.value;
this.adminService.saveRepositorySettings(settings).subscribe(
(savedSettings) => {
this.settings = savedSettings;
if (this.settings.authMethod === RepositoryAuthMethod.USERNAME_PASSWORD) {
this.showChangePassword = true;
this.changePassword = false;
} else {
this.showChangePrivateKeyPassword = true;
this.changePrivateKeyPassword = false;
}
this.repositorySettingsForm.reset(this.settings);
this.updateValidators(false);
this.store.dispatch(new ActionAuthUpdateHasRepository({ hasRepository: true }));
}
);
}
delete(formDirective: FormGroupDirective): void {
this.dialogService.confirm(
this.translate.instant('admin.delete-repository-settings-title', ),
this.translate.instant('admin.delete-repository-settings-text'), null,
this.translate.instant('action.delete')
).subscribe((data) => {
if (data) {
this.adminService.deleteRepositorySettings().subscribe(
() => {
this.settings = null;
this.showChangePassword = false;
this.changePassword = false;
this.showChangePrivateKeyPassword = false;
this.changePrivateKeyPassword = false;
formDirective.resetForm();
this.repositorySettingsForm.reset({ defaultBranch: 'main', authMethod: RepositoryAuthMethod.USERNAME_PASSWORD });
this.updateValidators(false);
this.store.dispatch(new ActionAuthUpdateHasRepository({ hasRepository: false }));
}
);
}
});
}
changePasswordChanged() {
if (this.changePassword) {
this.repositorySettingsForm.get('password').patchValue('');
this.repositorySettingsForm.get('password').markAsDirty();
}
this.updateValidators(false);
}
changePrivateKeyPasswordChanged() {
if (this.changePrivateKeyPassword) {
this.repositorySettingsForm.get('privateKeyPassword').patchValue('');
this.repositorySettingsForm.get('privateKeyPassword').markAsDirty();
}
this.updateValidators(false);
}
updateValidators(emitEvent?: boolean): void {
const authMethod: RepositoryAuthMethod = this.repositorySettingsForm.get('authMethod').value;
const privateKeyFileName: string = this.repositorySettingsForm.get('privateKeyFileName').value;
if (authMethod === RepositoryAuthMethod.USERNAME_PASSWORD) {
this.repositorySettingsForm.get('username').enable({emitEvent});
if (this.changePassword || !this.showChangePassword) {
this.repositorySettingsForm.get('password').enable({emitEvent});
} else {
this.repositorySettingsForm.get('password').disable({emitEvent});
}
this.repositorySettingsForm.get('privateKeyFileName').disable({emitEvent});
this.repositorySettingsForm.get('privateKey').disable({emitEvent});
this.repositorySettingsForm.get('privateKeyPassword').disable({emitEvent});
} else {
this.repositorySettingsForm.get('username').disable({emitEvent});
this.repositorySettingsForm.get('password').disable({emitEvent});
this.repositorySettingsForm.get('privateKeyFileName').enable({emitEvent});
this.repositorySettingsForm.get('privateKey').enable({emitEvent});
if (this.changePrivateKeyPassword || !this.showChangePrivateKeyPassword) {
this.repositorySettingsForm.get('privateKeyPassword').enable({emitEvent});
} else {
this.repositorySettingsForm.get('privateKeyPassword').disable({emitEvent});
}
if (isNotEmptyStr(privateKeyFileName)) {
this.repositorySettingsForm.get('privateKey').clearValidators();
} else {
this.repositorySettingsForm.get('privateKey').setValidators([Validators.required]);
}
}
this.repositorySettingsForm.get('username').updateValueAndValidity({emitEvent: false});
this.repositorySettingsForm.get('password').updateValueAndValidity({emitEvent: false});
this.repositorySettingsForm.get('privateKeyFileName').updateValueAndValidity({emitEvent: false});
this.repositorySettingsForm.get('privateKey').updateValueAndValidity({emitEvent: false});
this.repositorySettingsForm.get('privateKeyPassword').updateValueAndValidity({emitEvent: false});
}
}

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

@ -1,218 +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, Input, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { FormBuilder, FormGroup, FormGroupDirective, Validators } from '@angular/forms';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AdminService } from '@core/http/admin.service';
import {
EntitiesVersionControlSettings,
VersionControlAuthMethod,
versionControlAuthMethodTranslationMap
} from '@shared/models/settings.models';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { isNotEmptyStr } from '@core/utils';
import { DialogService } from '@core/services/dialog.service';
import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions';
import { ActionAuthUpdateHasVersionControl } from '@core/auth/auth.actions';
import { selectHasVersionControl } from '@core/auth/auth.selectors';
import { catchError, mergeMap, take } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({
selector: 'tb-version-control-settings',
templateUrl: './version-control-settings.component.html',
styleUrls: ['./version-control-settings.component.scss', './../../pages/admin/settings-card.scss']
})
export class VersionControlSettingsComponent extends PageComponent implements OnInit {
@Input()
detailsMode = false;
versionControlSettingsForm: FormGroup;
settings: EntitiesVersionControlSettings = null;
versionControlAuthMethod = VersionControlAuthMethod;
versionControlAuthMethods = Object.values(VersionControlAuthMethod);
versionControlAuthMethodTranslations = versionControlAuthMethodTranslationMap;
showChangePassword = false;
changePassword = false;
showChangePrivateKeyPassword = false;
changePrivateKeyPassword = false;
constructor(protected store: Store<AppState>,
private adminService: AdminService,
private dialogService: DialogService,
private translate: TranslateService,
public fb: FormBuilder) {
super(store);
}
ngOnInit() {
this.versionControlSettingsForm = this.fb.group({
repositoryUri: [null, [Validators.required]],
defaultBranch: ['main', []],
authMethod: [VersionControlAuthMethod.USERNAME_PASSWORD, [Validators.required]],
username: [null, []],
password: [null, []],
privateKeyFileName: [null, [Validators.required]],
privateKey: [null, []],
privateKeyPassword: [null, []]
});
this.updateValidators(false);
this.versionControlSettingsForm.get('authMethod').valueChanges.subscribe(() => {
this.updateValidators(true);
});
this.versionControlSettingsForm.get('privateKeyFileName').valueChanges.subscribe(() => {
this.updateValidators(false);
});
this.store.pipe(
select(selectHasVersionControl),
take(1),
mergeMap((hasVersionControl) => {
if (hasVersionControl) {
return this.adminService.getEntitiesVersionControlSettings({ignoreErrors: true}).pipe(
catchError(() => of(null))
);
} else {
return of(null);
}
})
).subscribe(
(settings) => {
this.settings = settings;
if (this.settings != null) {
if (this.settings.authMethod === VersionControlAuthMethod.USERNAME_PASSWORD) {
this.showChangePassword = true;
} else {
this.showChangePrivateKeyPassword = true;
}
this.versionControlSettingsForm.reset(this.settings);
this.updateValidators(false);
}
});
}
checkAccess(): void {
const settings: EntitiesVersionControlSettings = this.versionControlSettingsForm.value;
this.adminService.checkVersionControlAccess(settings).subscribe(() => {
this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.check-vc-access-success'),
type: 'success' }));
});
}
save(): void {
const settings: EntitiesVersionControlSettings = this.versionControlSettingsForm.value;
this.adminService.saveEntitiesVersionControlSettings(settings).subscribe(
(savedSettings) => {
this.settings = savedSettings;
if (this.settings.authMethod === VersionControlAuthMethod.USERNAME_PASSWORD) {
this.showChangePassword = true;
this.changePassword = false;
} else {
this.showChangePrivateKeyPassword = true;
this.changePrivateKeyPassword = false;
}
this.versionControlSettingsForm.reset(this.settings);
this.updateValidators(false);
this.store.dispatch(new ActionAuthUpdateHasVersionControl({ hasVersionControl: true }));
}
);
}
delete(formDirective: FormGroupDirective): void {
this.dialogService.confirm(
this.translate.instant('admin.delete-git-settings-title', ),
this.translate.instant('admin.delete-git-settings-text'), null,
this.translate.instant('action.delete')
).subscribe((data) => {
if (data) {
this.adminService.deleteEntitiesVersionControlSettings().subscribe(
() => {
this.settings = null;
this.showChangePassword = false;
this.changePassword = false;
this.showChangePrivateKeyPassword = false;
this.changePrivateKeyPassword = false;
formDirective.resetForm();
this.versionControlSettingsForm.reset({ defaultBranch: 'main', authMethod: VersionControlAuthMethod.USERNAME_PASSWORD });
this.updateValidators(false);
this.store.dispatch(new ActionAuthUpdateHasVersionControl({ hasVersionControl: false }));
}
);
}
});
}
changePasswordChanged() {
if (this.changePassword) {
this.versionControlSettingsForm.get('password').patchValue('');
this.versionControlSettingsForm.get('password').markAsDirty();
}
this.updateValidators(false);
}
changePrivateKeyPasswordChanged() {
if (this.changePrivateKeyPassword) {
this.versionControlSettingsForm.get('privateKeyPassword').patchValue('');
this.versionControlSettingsForm.get('privateKeyPassword').markAsDirty();
}
this.updateValidators(false);
}
updateValidators(emitEvent?: boolean): void {
const authMethod: VersionControlAuthMethod = this.versionControlSettingsForm.get('authMethod').value;
const privateKeyFileName: string = this.versionControlSettingsForm.get('privateKeyFileName').value;
if (authMethod === VersionControlAuthMethod.USERNAME_PASSWORD) {
this.versionControlSettingsForm.get('username').enable({emitEvent});
if (this.changePassword || !this.showChangePassword) {
this.versionControlSettingsForm.get('password').enable({emitEvent});
} else {
this.versionControlSettingsForm.get('password').disable({emitEvent});
}
this.versionControlSettingsForm.get('privateKeyFileName').disable({emitEvent});
this.versionControlSettingsForm.get('privateKey').disable({emitEvent});
this.versionControlSettingsForm.get('privateKeyPassword').disable({emitEvent});
} else {
this.versionControlSettingsForm.get('username').disable({emitEvent});
this.versionControlSettingsForm.get('password').disable({emitEvent});
this.versionControlSettingsForm.get('privateKeyFileName').enable({emitEvent});
this.versionControlSettingsForm.get('privateKey').enable({emitEvent});
if (this.changePrivateKeyPassword || !this.showChangePrivateKeyPassword) {
this.versionControlSettingsForm.get('privateKeyPassword').enable({emitEvent});
} else {
this.versionControlSettingsForm.get('privateKeyPassword').disable({emitEvent});
}
if (isNotEmptyStr(privateKeyFileName)) {
this.versionControlSettingsForm.get('privateKey').clearValidators();
} else {
this.versionControlSettingsForm.get('privateKey').setValidators([Validators.required]);
}
}
this.versionControlSettingsForm.get('username').updateValueAndValidity({emitEvent: false});
this.versionControlSettingsForm.get('password').updateValueAndValidity({emitEvent: false});
this.versionControlSettingsForm.get('privateKeyFileName').updateValueAndValidity({emitEvent: false});
this.versionControlSettingsForm.get('privateKey').updateValueAndValidity({emitEvent: false});
this.versionControlSettingsForm.get('privateKeyPassword').updateValueAndValidity({emitEvent: false});
}
}

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

@ -15,9 +15,9 @@
limitations under the License. limitations under the License.
--> -->
<tb-version-control-settings #versionControlSettingsComponent [detailsMode]="detailsMode" <tb-repository-settings #repositorySettingsComponent [detailsMode]="detailsMode"
*ngIf="!(hasVersionControl$ | async); else versionsTable"> *ngIf="!(hasRepository$ | async); else versionsTable">
</tb-version-control-settings> </tb-repository-settings>
<ng-template #versionsTable> <ng-template #versionsTable>
<tb-entity-versions-table [singleEntityMode]="singleEntityMode" <tb-entity-versions-table [singleEntityMode]="singleEntityMode"
[active]="active" [active]="active"

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

@ -17,9 +17,9 @@
import { Component, EventEmitter, Input, OnInit, Output, 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 { selectHasRepository } from '@core/auth/auth.selectors';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { VersionControlSettingsComponent } from '@home/components/vc/version-control-settings.component'; import { RepositorySettingsComponent } from '@home/components/vc/repository-settings.component';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
@ -30,7 +30,7 @@ import { EntityId } from '@shared/models/id/entity-id';
}) })
export class VersionControlComponent implements OnInit, HasConfirmForm { export class VersionControlComponent implements OnInit, HasConfirmForm {
@ViewChild('versionControlSettingsComponent', {static: false}) versionControlSettingsComponent: VersionControlSettingsComponent; @ViewChild('repositorySettingsComponent', {static: false}) repositorySettingsComponent: RepositorySettingsComponent;
@Input() @Input()
detailsMode = false; detailsMode = false;
@ -50,7 +50,7 @@ export class VersionControlComponent implements OnInit, HasConfirmForm {
@Output() @Output()
versionRestored = new EventEmitter<void>(); versionRestored = new EventEmitter<void>();
hasVersionControl$ = this.store.pipe(select(selectHasVersionControl)); hasRepository$ = this.store.pipe(select(selectHasRepository));
constructor(private store: Store<AppState>) { constructor(private store: Store<AppState>) {
@ -61,7 +61,7 @@ export class VersionControlComponent implements OnInit, HasConfirmForm {
} }
confirmForm(): FormGroup { confirmForm(): FormGroup {
return this.versionControlSettingsComponent?.versionControlSettingsForm; return this.repositorySettingsComponent?.repositorySettingsForm;
} }
} }

24
ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts

@ -33,7 +33,8 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { QueuesTableConfigResolver } from '@home/pages/admin/queue/queues-table-config.resolver'; import { QueuesTableConfigResolver } from '@home/pages/admin/queue/queues-table-config.resolver';
import { VersionControlAdminSettingsComponent } from '@home/pages/admin/version-control-admin-settings.component'; import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component';
import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component';
@Injectable() @Injectable()
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> { export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@ -225,17 +226,30 @@ const routes: Routes = [
] ]
}, },
{ {
path: 'vc', path: 'repository',
component: VersionControlAdminSettingsComponent, component: RepositoryAdminSettingsComponent,
canDeactivate: [ConfirmOnExitGuard], canDeactivate: [ConfirmOnExitGuard],
data: { data: {
auth: [Authority.TENANT_ADMIN], auth: [Authority.TENANT_ADMIN],
title: 'admin.git-settings', title: 'admin.repository-settings',
breadcrumb: { breadcrumb: {
label: 'admin.git-settings', label: 'admin.repository-settings',
icon: 'manage_history' icon: 'manage_history'
} }
} }
},
{
path: 'auto-commit',
component: AutoCommitAdminSettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.TENANT_ADMIN],
title: 'admin.auto-commit-settings',
breadcrumb: {
label: 'admin.auto-commit-settings',
icon: 'settings_backup_restore'
}
}
} }
] ]
} }

6
ui-ngx/src/app/modules/home/pages/admin/admin.module.ts

@ -29,7 +29,8 @@ import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dial
import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component'; import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component';
import { QueueComponent} from '@home/pages/admin/queue/queue.component'; import { QueueComponent} from '@home/pages/admin/queue/queue.component';
import { VersionControlAdminSettingsComponent } from '@home/pages/admin/version-control-admin-settings.component'; import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component';
import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -43,7 +44,8 @@ import { VersionControlAdminSettingsComponent } from '@home/pages/admin/version-
HomeSettingsComponent, HomeSettingsComponent,
ResourcesLibraryComponent, ResourcesLibraryComponent,
QueueComponent, QueueComponent,
VersionControlAdminSettingsComponent RepositoryAdminSettingsComponent,
AutoCommitAdminSettingsComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

23
ui-ngx/src/app/modules/home/pages/admin/auto-commit-admin-settings.component.html

@ -0,0 +1,23 @@
<!--
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.
-->
<tb-repository-settings #repositorySettingsComponent
*ngIf="!(hasRepository$ | async); else autoCommitSettings">
</tb-repository-settings>
<ng-template #autoCommitSettings>
<tb-auto-commit-settings #autoCommitSettingsComponent></tb-auto-commit-settings>
</ng-template>

51
ui-ngx/src/app/modules/home/pages/admin/auto-commit-admin-settings.component.ts

@ -0,0 +1,51 @@
///
/// 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, OnInit, ViewChild } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormGroup } from '@angular/forms';
import { AutoCommitSettingsComponent } from '@home/components/vc/auto-commit-settings.component';
import { selectHasRepository } from '@core/auth/auth.selectors';
import { RepositorySettingsComponent } from '@home/components/vc/repository-settings.component';
@Component({
selector: 'tb-auto-commit-admin-settings',
templateUrl: './auto-commit-admin-settings.component.html',
styleUrls: []
})
export class AutoCommitAdminSettingsComponent extends PageComponent implements OnInit, HasConfirmForm {
@ViewChild('repositorySettingsComponent', {static: false}) repositorySettingsComponent: RepositorySettingsComponent;
@ViewChild('autoCommitSettingsComponent', {static: false}) autoCommitSettingsComponent: AutoCommitSettingsComponent;
hasRepository$ = this.store.pipe(select(selectHasRepository));
constructor(protected store: Store<AppState>) {
super(store);
}
ngOnInit() {
}
confirmForm(): FormGroup {
return this.repositorySettingsComponent ?
this.repositorySettingsComponent?.repositorySettingsForm :
this.autoCommitSettingsComponent?.autoCommitSettingsForm;
}
}

2
ui-ngx/src/app/modules/home/pages/admin/version-control-admin-settings.component.html → ui-ngx/src/app/modules/home/pages/admin/repository-admin-settings.component.html

@ -15,4 +15,4 @@
limitations under the License. limitations under the License.
--> -->
<tb-version-control-settings #versionControlSettingsComponent></tb-version-control-settings> <tb-repository-settings #repositorySettingsComponent></tb-repository-settings>

12
ui-ngx/src/app/modules/home/pages/admin/version-control-admin-settings.component.ts → ui-ngx/src/app/modules/home/pages/admin/repository-admin-settings.component.ts

@ -20,16 +20,16 @@ import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { VersionControlSettingsComponent } from '@home/components/vc/version-control-settings.component'; import { RepositorySettingsComponent } from '@home/components/vc/repository-settings.component';
@Component({ @Component({
selector: 'tb-version-control-admin-settings', selector: 'tb-repository-admin-settings',
templateUrl: './version-control-admin-settings.component.html', templateUrl: './repository-admin-settings.component.html',
styleUrls: [] styleUrls: []
}) })
export class VersionControlAdminSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { export class RepositoryAdminSettingsComponent extends PageComponent implements OnInit, HasConfirmForm {
@ViewChild('versionControlSettingsComponent') versionControlSettingsComponent: VersionControlSettingsComponent; @ViewChild('repositorySettingsComponent') repositorySettingsComponent: RepositorySettingsComponent;
constructor(protected store: Store<AppState>) { constructor(protected store: Store<AppState>) {
super(store); super(store);
@ -39,6 +39,6 @@ export class VersionControlAdminSettingsComponent extends PageComponent implemen
} }
confirmForm(): FormGroup { confirmForm(): FormGroup {
return this.versionControlSettingsComponent?.versionControlSettingsForm; return this.repositorySettingsComponent?.repositorySettingsForm;
} }
} }

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

@ -15,9 +15,9 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field [formGroup]="branchFormGroup" class="mat-block" [floatLabel]="selectionMode ? 'always' : 'auto'"> <mat-form-field [formGroup]="branchFormGroup" class="mat-block" [floatLabel]="(selectionMode || emptyPlaceholder) ? 'always' : 'auto'">
<mat-label>{{ 'version-control.branch' | translate }}</mat-label> <mat-label>{{ 'version-control.branch' | translate }}</mat-label>
<input matInput type="text" placeholder="{{(loading ? 'common.loading' : 'version-control.select-branch') | translate}}" <input matInput type="text" placeholder="{{emptyPlaceholder || ((loading ? 'common.loading' : 'version-control.select-branch') | translate)}}"
#branchInput #branchInput
formControlName="branch" formControlName="branch"
(keydown.enter)="branchInput.blur(); autoCompleteTrigger.closePanel();" (keydown.enter)="branchInput.blur(); autoCompleteTrigger.closePanel();"

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

@ -86,6 +86,9 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
@Input() @Input()
selectionMode = false; selectionMode = false;
@Input()
emptyPlaceholder: string;
@ViewChild('branchAutocomplete') matAutocomplete: MatAutocomplete; @ViewChild('branchAutocomplete') matAutocomplete: MatAutocomplete;
@ViewChild('branchInput', { read: MatAutocompleteTrigger, static: true }) autoCompleteTrigger: MatAutocompleteTrigger; @ViewChild('branchInput', { read: MatAutocompleteTrigger, static: true }) autoCompleteTrigger: MatAutocompleteTrigger;
@ViewChild('branchInput', {static: true}) branchInput: ElementRef; @ViewChild('branchInput', {static: true}) branchInput: ElementRef;

3
ui-ngx/src/app/shared/models/constants.ts

@ -135,7 +135,8 @@ export const HelpLinks = {
ruleNodePushToCloud: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-cloud', ruleNodePushToCloud: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-cloud',
ruleNodePushToEdge: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-edge', ruleNodePushToEdge: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-edge',
queue: helpBaseUrl + '/docs/user-guide/queue', queue: helpBaseUrl + '/docs/user-guide/queue',
versionControlSettings: helpBaseUrl + '/docs/user-guide/ui/version-control-settings' repositorySettings: helpBaseUrl + '/docs/user-guide/ui/repository-settings',
autoCommitSettings: helpBaseUrl + '/docs/user-guide/ui/auto-commit-settings',
} }
}; };

19
ui-ngx/src/app/shared/models/settings.models.ts

@ -16,6 +16,7 @@
import { ValidatorFn } from '@angular/forms'; import { ValidatorFn } from '@angular/forms';
import { isNotEmptyStr, isNumber } from '@core/utils'; import { isNotEmptyStr, isNumber } from '@core/utils';
import { VersionCreateConfig } from '@shared/models/vc.models';
export const smtpPortPattern: RegExp = /^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/; export const smtpPortPattern: RegExp = /^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/;
@ -397,23 +398,29 @@ export function createSmsProviderConfiguration(type: SmsProviderType): SmsProvid
return smsProviderConfiguration; return smsProviderConfiguration;
} }
export enum VersionControlAuthMethod { export enum RepositoryAuthMethod {
USERNAME_PASSWORD = 'USERNAME_PASSWORD', USERNAME_PASSWORD = 'USERNAME_PASSWORD',
PRIVATE_KEY = 'PRIVATE_KEY' PRIVATE_KEY = 'PRIVATE_KEY'
} }
export const versionControlAuthMethodTranslationMap = new Map<VersionControlAuthMethod, string>([ export const repositoryAuthMethodTranslationMap = new Map<RepositoryAuthMethod, string>([
[VersionControlAuthMethod.USERNAME_PASSWORD, 'admin.auth-method-username-password'], [RepositoryAuthMethod.USERNAME_PASSWORD, 'admin.auth-method-username-password'],
[VersionControlAuthMethod.PRIVATE_KEY, 'admin.auth-method-private-key'] [RepositoryAuthMethod.PRIVATE_KEY, 'admin.auth-method-private-key']
]); ]);
export interface EntitiesVersionControlSettings { export interface RepositorySettings {
repositoryUri: string; repositoryUri: string;
defaultBranch: string; defaultBranch: string;
authMethod: VersionControlAuthMethod; authMethod: RepositoryAuthMethod;
username: string; username: string;
password: string; password: string;
privateKeyFileName: string; privateKeyFileName: string;
privateKey: string; privateKey: string;
privateKeyPassword: string; privateKeyPassword: string;
} }
export interface AutoVersionCreateConfig extends VersionCreateConfig {
branch: string;
}
export type AutoCommitSettings = {[entityType: string]: AutoVersionCreateConfig};

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

@ -32,6 +32,7 @@ export const exportableEntityTypes: Array<EntityType> = [
export interface VersionCreateConfig { export interface VersionCreateConfig {
saveRelations: boolean; saveRelations: boolean;
saveAttributes: boolean;
} }
export enum VersionCreateRequestType { export enum VersionCreateRequestType {
@ -81,6 +82,7 @@ export function createDefaultEntityTypesVersionCreate(): {[entityType: string]:
res[entityType] = { res[entityType] = {
syncStrategy: null, syncStrategy: null,
saveRelations: false, saveRelations: false,
saveAttributes: false,
allEntities: true, allEntities: true,
entityIds: [] entityIds: []
}; };
@ -90,6 +92,7 @@ export function createDefaultEntityTypesVersionCreate(): {[entityType: string]:
export interface VersionLoadConfig { export interface VersionLoadConfig {
loadRelations: boolean; loadRelations: boolean;
loadAttributes: boolean;
} }
export enum VersionLoadRequestType { export enum VersionLoadRequestType {
@ -111,6 +114,7 @@ export interface SingleEntityVersionLoadRequest extends VersionLoadRequest {
export interface EntityTypeVersionLoadConfig extends VersionLoadConfig { export interface EntityTypeVersionLoadConfig extends VersionLoadConfig {
removeOtherEntities: boolean; removeOtherEntities: boolean;
findExistingEntityByName: boolean;
} }
export interface EntityTypeVersionLoadRequest extends VersionLoadRequest { export interface EntityTypeVersionLoadRequest extends VersionLoadRequest {
@ -123,7 +127,9 @@ export function createDefaultEntityTypesVersionLoad(): {[entityType: string]: En
for (const entityType of exportableEntityTypes) { for (const entityType of exportableEntityTypes) {
res[entityType] = { res[entityType] = {
loadRelations: false, loadRelations: false,
removeOtherEntities: false loadAttributes: false,
removeOtherEntities: false,
findExistingEntityByName: false
}; };
} }
return res; return res;
@ -138,6 +144,7 @@ export interface EntityVersion {
timestamp: number; timestamp: number;
id: string; id: string;
name: string; name: string;
author: string;
} }
export interface VersionCreationResult { export interface VersionCreationResult {
@ -175,18 +182,5 @@ export interface EntityDataDiff {
} }
export function entityExportDataToJsonString(data: EntityExportData<any>): string { export function entityExportDataToJsonString(data: EntityExportData<any>): string {
if (!data.relations) { return JSON.stringify(data, null, 4);
data.relations = [];
}
const allKeys = new Set<string>();
JSON.stringify(data, (key, value) => (allKeys.add(key), value));
return JSON.stringify(data, Array.from(allKeys).sort((key1, key2) => {
if (key1 === 'relations') {
return 1;
} else if (key2 === 'relations') {
return -1;
} else {
return 0;
}
}), 4);
} }

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

@ -58,7 +58,8 @@
"next-with-label": "Next: {{label}}", "next-with-label": "Next: {{label}}",
"read-more": "Read more", "read-more": "Read more",
"hide": "Hide", "hide": "Hide",
"restore": "Restore" "restore": "Restore",
"confirm": "Confirm"
}, },
"aggregation": { "aggregation": {
"aggregation": "Aggregation", "aggregation": "Aggregation",
@ -321,8 +322,7 @@
"queue-submit-strategy": "Submit strategy", "queue-submit-strategy": "Submit strategy",
"queue-processing-strategy": "Processing strategy", "queue-processing-strategy": "Processing strategy",
"queue-configuration": "Queue configuration", "queue-configuration": "Queue configuration",
"git-settings": "Git settings", "repository-settings": "Repository settings",
"git-repository-settings": "Git repository settings",
"repository-url": "Repository URL", "repository-url": "Repository URL",
"repository-url-required": "Repository URL is required.", "repository-url-required": "Repository URL is required.",
"default-branch": "Default branch name", "default-branch": "Default branch name",
@ -338,9 +338,14 @@
"enter-passphrase": "Enter passphrase", "enter-passphrase": "Enter passphrase",
"change-passphrase": "Change passphrase", "change-passphrase": "Change passphrase",
"check-access": "Check access", "check-access": "Check access",
"check-vc-access-success": "Git repository access successfully verified!", "check-repository-access-success": "Repository access successfully verified!",
"delete-git-settings-title": "Are you sure you want to delete git settings?", "delete-repository-settings-title": "Are you sure you want to delete repository settings?",
"delete-git-settings-text": "Be careful, after the confirmation the git settings will be removed and git synchronization feature will be unavailable." "delete-repository-settings-text": "Be careful, after the confirmation the repository settings will be removed and version control feature will be unavailable.",
"auto-commit-settings": "Auto-commit settings",
"auto-commit-entities": "Auto-commit entities",
"no-auto-commit-entities-prompt": "No entities configured for auto-commit",
"delete-auto-commit-settings-title": "Are you sure you want to delete auto-commit settings?",
"delete-auto-commit-settings-text": "Be careful, after the confirmation the auto-commit settings will be removed and auto-commit will be disabled for all entities."
}, },
"alarm": { "alarm": {
"alarm": "Alarm", "alarm": "Alarm",
@ -3123,7 +3128,9 @@
"create-entity-version": "Create entity version", "create-entity-version": "Create entity version",
"version-name": "Version name", "version-name": "Version name",
"version-name-required": "Version name is required", "version-name-required": "Version name is required",
"author": "Author",
"export-entity-relations": "Export entity relations", "export-entity-relations": "Export entity relations",
"export-entity-attributes": "Export entity attributes",
"entity-versions": "Entity versions", "entity-versions": "Entity versions",
"versions": "Versions", "versions": "Versions",
"created-time": "Created time", "created-time": "Created time",
@ -3136,6 +3143,7 @@
"restore-version": "Restore version", "restore-version": "Restore version",
"restore-entity-from-version": "Restore entity from version '{{versionName}}'", "restore-entity-from-version": "Restore entity from version '{{versionName}}'",
"load-entity-relations": "Load entity relations", "load-entity-relations": "Load entity relations",
"load-entity-attributes": "Load entity attributes",
"show-version-diff": "Show version diff", "show-version-diff": "Show version diff",
"diff-entity-with-version": "Diff with entity version '{{versionName}}'", "diff-entity-with-version": "Diff with entity version '{{versionName}}'",
"previous-difference": "Previous Difference", "previous-difference": "Previous Difference",
@ -3146,20 +3154,26 @@
"default-sync-strategy": "Default sync strategy", "default-sync-strategy": "Default sync strategy",
"sync-strategy-merge": "Merge", "sync-strategy-merge": "Merge",
"sync-strategy-overwrite": "Overwrite", "sync-strategy-overwrite": "Overwrite",
"entity-types": "Entity types", "entities-to-export": "Entities to export",
"entities-to-restore": "Entities to restore",
"sync-strategy": "Sync strategy", "sync-strategy": "Sync strategy",
"all-entities": "All entities", "all-entities": "All entities",
"no-entity-types": "No entity types configured", "no-entities-to-export-prompt": "Please specify entities to export",
"no-entities-to-restore-prompt": "Please specify entities to restore",
"add-entity-type": "Add entity type", "add-entity-type": "Add entity type",
"remove-all": "Remove all", "remove-all": "Remove all",
"version-create-result": "{ added, plural, 0 {No entities} 1 {1 entity} other {# entities} } added.\n{ modified, plural, 0 {No entities} 1 {1 entity} other {# entities} } modified.\n{ removed, plural, 0 {No entities} 1 {1 entity} other {# entities} } removed.", "version-create-result": "{ added, plural, 0 {No entities} 1 {1 entity} other {# entities} } added.\n{ modified, plural, 0 {No entities} 1 {1 entity} other {# entities} } modified.\n{ removed, plural, 0 {No entities} 1 {1 entity} other {# entities} } removed.",
"load-entities-relations": "Load entities relations", "load-entities-relations": "Load entities relations",
"load-entities-attributes": "Load entities attributes",
"remove-other-entities": "Remove other entities", "remove-other-entities": "Remove other entities",
"find-existing-entity-by-name": "Find existing entity by name",
"restore-entities-from-version": "Restore entities from version '{{versionName}}'", "restore-entities-from-version": "Restore entities from version '{{versionName}}'",
"no-entities-restored": "No entities restored", "no-entities-restored": "No entities restored",
"created": "{{created}} created", "created": "{{created}} created",
"updated": "{{updated}} updated", "updated": "{{updated}} updated",
"deleted": "{{deleted}} deleted" "deleted": "{{deleted}} deleted",
"remove-other-entities-confirm-text": "Be careful! This will permanently <b>delete</b> <b>all</b> current entities<br/>not present in the version you want to restore.<br/><br/>Please type <b>remove other entities</b> to confirm.",
"auto-commit-to-branch": "auto-commit to <b>{{ branch }}</b> branch"
}, },
"widget": { "widget": {
"widget-library": "Widgets Library", "widget-library": "Widgets Library",

Loading…
Cancel
Save