diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index bf8c656257..45b8aa5f95 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -207,10 +207,14 @@ public class AdminController extends BaseController { notes = "Creates or Updates the version control settings object. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping("/vcSettings") - public void saveVersionControlSettings(@RequestBody EntitiesVersionControlSettings settings) throws ThingsboardException { + public EntitiesVersionControlSettings saveVersionControlSettings(@RequestBody EntitiesVersionControlSettings settings) throws ThingsboardException { try { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); - versionControlService.saveVersionControlSettings(getTenantId(), settings); + EntitiesVersionControlSettings versionControlSettings = checkNotNull(versionControlService.saveVersionControlSettings(getTenantId(), settings)); + versionControlSettings.setPassword(null); + versionControlSettings.setPrivateKey(null); + versionControlSettings.setPrivateKeyPassword(null); + return versionControlSettings; } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/ClearRepositoryGitRequest.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/ClearRepositoryGitRequest.java new file mode 100644 index 0000000000..86472e7882 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/ClearRepositoryGitRequest.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; + +import org.thingsboard.server.common.data.id.TenantId; + +public class ClearRepositoryGitRequest extends VoidGitRequest { + + public ClearRepositoryGitRequest(TenantId tenantId) { + super(tenantId); + } + + public boolean requiresSettings() { + return false; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/CommitGitRequest.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/CommitGitRequest.java index b510ffa62c..3c83a34cfe 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/CommitGitRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/CommitGitRequest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.sync.vc; +import lombok.Getter; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; import org.thingsboard.server.common.data.sync.vc.request.create.VersionCreateRequest; @@ -23,10 +24,13 @@ import java.util.UUID; public class CommitGitRequest extends PendingGitRequest { + @Getter + private final UUID txId; private final VersionCreateRequest request; public CommitGitRequest(TenantId tenantId, VersionCreateRequest request) { super(tenantId); + this.txId = UUID.randomUUID(); this.request = request; } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java index 862a6966c2..3475ec0e0b 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java @@ -321,11 +321,21 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont @Override public EntitiesVersionControlSettings saveVersionControlSettings(TenantId tenantId, EntitiesVersionControlSettings versionControlSettings) { - EntitiesVersionControlSettings storedSettings = getVersionControlSettings(tenantId); + AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, SETTINGS_KEY); + EntitiesVersionControlSettings storedSettings = null; + if (adminSettings != null) { + try { + storedSettings = JacksonUtil.convertValue(adminSettings.getJsonValue(), EntitiesVersionControlSettings.class); + } catch (Exception e) { + throw new RuntimeException("Failed to load version control settings!", e); + } + } versionControlSettings = this.restoreCredentials(versionControlSettings, storedSettings); - AdminSettings adminSettings = new AdminSettings(); - adminSettings.setTenantId(tenantId); - adminSettings.setKey(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; @@ -359,11 +369,21 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont //TODO: ashvayka: replace future.get with deferred result. gitServiceQueue.testRepository(tenantId, settings).get(); } catch (Exception e) { - throw new ThingsboardException(String.format("Unable to access repository: %s", e.getMessage()), + throw new ThingsboardException(String.format("Unable to access repository: %s", getCauseMessage(e)), ThingsboardErrorCode.GENERAL); } } + private String getCauseMessage(Exception e) { + String message; + if(e.getCause() != null && StringUtils.isNotEmpty(e.getCause().getMessage())){ + message = e.getCause().getMessage(); + } else { + message = e.getMessage(); + } + return message; + } + private EntitiesVersionControlSettings restoreCredentials(EntitiesVersionControlSettings settings, EntitiesVersionControlSettings storedSettings) { VersionControlAuthMethod authMethod = settings.getAuthMethod(); if (VersionControlAuthMethod.USERNAME_PASSWORD.equals(authMethod) && settings.getPassword() == null) { @@ -373,8 +393,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont } else if (VersionControlAuthMethod.PRIVATE_KEY.equals(authMethod) && settings.getPrivateKey() == null) { if (storedSettings != null) { settings.setPrivateKey(storedSettings.getPrivateKey()); - if (StringUtils.isEmpty(settings.getPrivateKeyPassword()) && - StringUtils.isNotEmpty(storedSettings.getPrivateKeyPassword())) { + if (settings.getPrivateKeyPassword() == null) { settings.setPrivateKeyPassword(storedSettings.getPrivateKeyPassword()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java index 549c6421e4..7ffcbb9c14 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java @@ -20,10 +20,14 @@ import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializationFeature; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; +import com.google.protobuf.ByteString; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.StringUtils; @@ -48,6 +52,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.VersionControlRespon import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbCoreComponent; import java.io.IOException; @@ -59,14 +64,25 @@ import java.util.function.Function; @TbCoreComponent @Service -@RequiredArgsConstructor @Slf4j public class DefaultGitVersionControlQueueService implements GitVersionControlQueueService { - private final ObjectWriter jsonWriter = new ObjectMapper().writer(SerializationFeature.INDENT_OUTPUT); private final TbServiceInfoProvider serviceInfoProvider; private final TbClusterService clusterService; + private final DataDecodingEncodingService encodingService; + private final DefaultEntitiesVersionControlService entitiesVersionControlService; + private final Map> pendingRequestMap = new HashMap<>(); + private final ObjectWriter jsonWriter = new ObjectMapper().writer(SerializationFeature.INDENT_OUTPUT); + + public DefaultGitVersionControlQueueService(TbServiceInfoProvider serviceInfoProvider, TbClusterService clusterService, + DataDecodingEncodingService encodingService, + @Lazy DefaultEntitiesVersionControlService entitiesVersionControlService) { + this.serviceInfoProvider = serviceInfoProvider; + this.clusterService = clusterService; + this.encodingService = encodingService; + this.entitiesVersionControlService = entitiesVersionControlService; + } @Override public ListenableFuture prepareCommit(TenantId tenantId, VersionCreateRequest request) { @@ -74,9 +90,8 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu CommitGitRequest commit = new CommitGitRequest(tenantId, request); registerAndSend(commit, builder -> builder.setCommitRequest( - CommitRequestMsg.newBuilder().setPrepareMsg(getCommitPrepareMsg(request)).build() + buildCommitRequest(commit).setPrepareMsg(getCommitPrepareMsg(request)).build() ).build(), wrap(future, commit)); - return future; } @@ -94,11 +109,11 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu } registerAndSend(commit, builder -> builder.setCommitRequest( - CommitRequestMsg.newBuilder().setAddMsg( + buildCommitRequest(commit).setAddMsg( TransportProtos.AddMsg.newBuilder() .setRelativePath(path).setEntityDataJson(entityDataJson).build() ).build() - ).build(), wrap(commit.getFuture(), null)); + ).build(), wrap(future, null)); return future; } @@ -109,7 +124,7 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu String path = getRelativePath(entityType, null); registerAndSend(commit, builder -> builder.setCommitRequest( - CommitRequestMsg.newBuilder().setDeleteMsg( + buildCommitRequest(commit).setDeleteMsg( TransportProtos.DeleteMsg.newBuilder().setRelativePath(path).build() ).build() ).build(), wrap(commit.getFuture(), null)); @@ -120,7 +135,7 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu @Override public ListenableFuture push(CommitGitRequest commit) { registerAndSend(commit, builder -> builder.setCommitRequest( - CommitRequestMsg.newBuilder().setPushMsg( + buildCommitRequest(commit).setPushMsg( TransportProtos.PushMsg.newBuilder().build() ).build() ).build(), wrap(commit.getFuture())); @@ -202,7 +217,6 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu .setEntityIdMSB(entityId.getId().getMostSignificantBits()) .setEntityIdLSB(entityId.getId().getLeastSignificantBits())).build() , wrap(request.getFuture())); - return request.getFuture(); // try { // String entityDataJson = gitRepositoryService.getFileContentAtCommit(tenantId, @@ -214,10 +228,16 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu // } } - private void registerAndSend(PendingGitRequest request, Function enrichFunction, TbQueueCallback callback) { + private void registerAndSend(PendingGitRequest request, + Function enrichFunction, TbQueueCallback callback) { + registerAndSend(request, enrichFunction, null, callback); + } + + private void registerAndSend(PendingGitRequest request, + Function enrichFunction, EntitiesVersionControlSettings settings, TbQueueCallback callback) { if (!request.getFuture().isDone()) { pendingRequestMap.putIfAbsent(request.getRequestId(), request); - clusterService.pushMsgToVersionControl(request.getTenantId(), enrichFunction.apply(newRequestProto(request)), callback); + clusterService.pushMsgToVersionControl(request.getTenantId(), enrichFunction.apply(newRequestProto(request, settings)), callback); } else { throw new RuntimeException("Future is already done!"); } @@ -243,7 +263,7 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu VoidGitRequest request = new VoidGitRequest(tenantId); registerAndSend(request, builder -> builder.setInitRepositoryRequest(GenericRepositoryRequestMsg.newBuilder().build()).build() - , wrap(request.getFuture())); + , settings, wrap(request.getFuture())); return request.getFuture(); } @@ -252,15 +272,16 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu public ListenableFuture testRepository(TenantId tenantId, EntitiesVersionControlSettings settings) { VoidGitRequest request = new VoidGitRequest(tenantId); - registerAndSend(request, builder -> builder.setTestRepositoryRequest(GenericRepositoryRequestMsg.newBuilder().build()).build() - , wrap(request.getFuture())); + registerAndSend(request, builder -> builder + .setTestRepositoryRequest(GenericRepositoryRequestMsg.newBuilder().build()).build() + , settings, wrap(request.getFuture())); return request.getFuture(); } @Override public ListenableFuture clearRepository(TenantId tenantId) { - VoidGitRequest request = new VoidGitRequest(tenantId); + ClearRepositoryGitRequest request = new ClearRepositoryGitRequest(tenantId); registerAndSend(request, builder -> builder.setClearRepositoryRequest(GenericRepositoryRequestMsg.newBuilder().build()).build() , wrap(request.getFuture())); @@ -284,6 +305,14 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu } else { if (vcResponseMsg.hasGenericResponse()) { future.set(null); + } else if (vcResponseMsg.hasCommitResponse()) { + var commitResponse = vcResponseMsg.getCommitResponse(); + var commitResult = new VersionCreationResult(); + commitResult.setVersion(new EntityVersion(commitResponse.getCommitId(), commitResponse.getName())); + commitResult.setAdded(commitResponse.getAdded()); + commitResult.setRemoved(commitResponse.getRemoved()); + commitResult.setModified(commitResponse.getModified()); + ((CommitGitRequest) request).getFuture().set(commitResult); } } } @@ -327,16 +356,29 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu return PrepareMsg.newBuilder().setCommitMsg(request.getVersionName()).setBranchName(request.getBranch()).build(); } - private ToVersionControlServiceMsg.Builder newRequestProto(PendingGitRequest request) { + private ToVersionControlServiceMsg.Builder newRequestProto(PendingGitRequest request, EntitiesVersionControlSettings settings) { var tenantId = request.getTenantId(); var requestId = request.getRequestId(); - return ToVersionControlServiceMsg.newBuilder() + var builder = ToVersionControlServiceMsg.newBuilder() .setNodeId(serviceInfoProvider.getServiceId()) .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setRequestIdMSB(requestId.getMostSignificantBits()) .setRequestIdLSB(requestId.getLeastSignificantBits()); + EntitiesVersionControlSettings vcSettings = settings; + if (vcSettings == null && request.requiresSettings()) { + vcSettings = entitiesVersionControlService.getVersionControlSettings(tenantId); + } + if (vcSettings != null) { + builder.setVcSettings(ByteString.copyFrom(encodingService.encode(vcSettings))); + } else { + throw new RuntimeException("No entity version control settings provisioned!"); + } + return builder; + } + private CommitRequestMsg.Builder buildCommitRequest(CommitGitRequest commit) { + return CommitRequestMsg.newBuilder().setTxId(commit.getTxId().toString()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/LocalGitVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/LocalGitVersionControlService.java index 66803e062d..d995472531 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/LocalGitVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/LocalGitVersionControlService.java @@ -63,7 +63,7 @@ public class LocalGitVersionControlService { private final TenantDao tenantDao; private final AdminSettingsService adminSettingsService; private final ConcurrentMap tenantRepoLocks = new ConcurrentHashMap<>(); -// private final Map pendingCommitMap = new HashMap<>(); + private final Map pendingCommitMap = new HashMap<>(); // // @AfterStartUp // public void init() { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/PendingGitRequest.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/PendingGitRequest.java index 09bc272828..9b612cee5b 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/PendingGitRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/PendingGitRequest.java @@ -33,4 +33,8 @@ public class PendingGitRequest { this.tenantId = tenantId; this.future = SettableFuture.create(); } + + public boolean requiresSettings(){ + return true; + } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 3fb48d2789..76965089f9 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1122,6 +1122,7 @@ metrics: vc: thread_pool_size: "${TB_VC_POOL_SIZE:4}" git: + folder: "${TB_VC_GIT_FOLDER:}" repos-poll-interval: "${TB_VC_GIT_REPOS_POLL_INTERVAL_SEC:60}" management: diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index 7a5030277e..6d45698451 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -680,11 +680,12 @@ message EdgeNotificationMsgProto { TB Core to Version Control Service */ message CommitRequestMsg { - PrepareMsg prepareMsg = 1; - AddMsg addMsg = 2; - DeleteMsg deleteMsg = 3; - PushMsg pushMsg = 4; - AbortMsg abortMsg = 5; + string txId = 1; // To correlate prepare, add, delete and push messages + PrepareMsg prepareMsg = 2; + AddMsg addMsg = 3; + DeleteMsg deleteMsg = 4; + PushMsg pushMsg = 5; + AbortMsg abortMsg = 6; } message CommitResponseMsg { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbApplicationEventListener.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbApplicationEventListener.java index 31bad5477e..7b5c142c23 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbApplicationEventListener.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbApplicationEventListener.java @@ -40,7 +40,7 @@ public abstract class TbApplicationEventListener i } finally { seqNumberLock.unlock(); } - if (validUpdate) { + if (validUpdate && filterTbApplicationEvent(event)) { onTbApplicationEvent(event); } else { log.info("Application event ignored due to invalid sequence number ({} > {}). Event: {}", lastProcessedSequenceNumber, event.getSequenceNumber(), event); @@ -49,5 +49,8 @@ public abstract class TbApplicationEventListener i protected abstract void onTbApplicationEvent(T event); + protected boolean filterTbApplicationEvent(T event) { + return true; + } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index b6b89fcc2f..815e0efdf8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -157,8 +157,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE @Override public TbQueueProducer> createVersionControlMsgProducer() { - //TODO: version-control - return null; + return new InMemoryTbQueueProducer<>(storage, vcSettings.getTopic()); } @Scheduled(fixedRateString = "${queue.in_memory.stats.print-interval-ms:60000}") diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java index 73089acdf5..b9c595beac 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java @@ -17,28 +17,25 @@ package org.thingsboard.server.service.sync.vc; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.annotation.Lazy; import org.springframework.context.event.EventListener; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.sync.vc.EntitiesVersionControlSettings; -import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.*; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; -import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueConsumer; -import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.NotificationsTopicService; -import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbApplicationEventListener; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; @@ -48,11 +45,19 @@ import org.thingsboard.server.queue.util.TbVersionControlComponent; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; +import java.io.IOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; @Slf4j @TbVersionControlComponent @@ -66,6 +71,9 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe private final GitRepositoryService vcService; private final NotificationsTopicService notificationsTopicService; + private final ConcurrentMap tenantRepoLocks = new ConcurrentHashMap<>(); + private final Map pendingCommitMap = new HashMap<>(); + private volatile ExecutorService consumerExecutor; private volatile TbQueueConsumer> consumer; private volatile TbQueueProducer> producer; @@ -100,6 +108,11 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe consumer.subscribe(event.getPartitions()); } + @Override + protected boolean filterTbApplicationEvent(PartitionChangeEvent event) { + return ServiceType.TB_VC_EXECUTOR.equals(event.getServiceType()); + } + @EventListener(ApplicationReadyEvent.class) @Order(value = 2) public void onApplicationEvent(ApplicationReadyEvent event) { @@ -115,36 +128,35 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe } for (TbProtoQueueMsg msgWrapper : msgs) { ToVersionControlServiceMsg msg = msgWrapper.getValue(); - if (msg.hasClearRepositoryRequest()) { - handleClearRepositoryCommand(new VersionControlRequestCtx(msg, null)); - } else { - VersionControlRequestCtx ctx = new VersionControlRequestCtx(msg, getEntitiesVersionControlSettings(msg)); - if (msg.hasTestRepositoryRequest()) { - handleTestRepositoryCommand(ctx); - } else if (msg.hasInitRepositoryRequest()) { - handleInitRepositoryCommand(ctx); + var ctx = new VersionControlRequestCtx(msg, msg.hasClearRepositoryRequest() ? null : getEntitiesVersionControlSettings(msg)); + var lock = getRepoLock(ctx.getTenantId()); + lock.lock(); + try { + if (msg.hasClearRepositoryRequest()) { + handleClearRepositoryCommand(ctx); + } else { + if (msg.hasTestRepositoryRequest()) { + handleTestRepositoryCommand(ctx); + } else if (msg.hasInitRepositoryRequest()) { + handleInitRepositoryCommand(ctx); + } else { + var currentSettings = vcService.getRepositorySettings(ctx.getTenantId()); + var newSettings = ctx.getSettings(); + if (!newSettings.equals(currentSettings)) { + vcService.initRepository(ctx.getTenantId(), ctx.getSettings()); + } + if (msg.hasCommitRequest()) { + handleCommitRequest(ctx, msg.getCommitRequest()); + } + } } + } catch (Exception e) { + reply(ctx, Optional.of(e)); + } finally { + lock.unlock(); } } -// ConcurrentMap> pendingMap = msgs.stream().collect( -// Collectors.toConcurrentMap(s -> UUID.randomUUID(), Function.identity())); -// CountDownLatch processingTimeoutLatch = new CountDownLatch(1); -// TbPackProcessingContext> ctx = new TbPackProcessingContext<>( -// processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); -// pendingMap.forEach((id, msg) -> { -// log.trace("[{}] Creating downlink callback for message: {}", id, msg.getValue()); -// TbCallback callback = new TbPackCallback<>(id, ctx); -// try { -// handleDownlink(id, msg, callback); -// } catch (Throwable e) { -// log.warn("[{}] Failed to process notification: {}", id, msg, e); -// callback.onFailure(e); -// } -// }); -// if (!processingTimeoutLatch.await(processingTimeout, TimeUnit.MILLISECONDS)) { -// ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process downlink: {}", id, msg.getValue())); -// ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process downlink: {}", id, msg.getValue())); -// } + //TODO: handle timeouts and async processing for multiple tenants; consumer.commit(); } catch (Exception e) { if (!stopped) { @@ -160,6 +172,66 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe log.info("TB Version Control request consumer stopped."); } + private void handleCommitRequest(VersionControlRequestCtx ctx, CommitRequestMsg commitRequest) throws Exception { + var tenantId = ctx.getTenantId(); + UUID txId = UUID.fromString(commitRequest.getTxId()); + if (commitRequest.hasPrepareMsg()) { + prepareCommit(ctx, txId, commitRequest.getPrepareMsg()); + } else if (commitRequest.hasAbortMsg()) { + PendingCommit current = pendingCommitMap.get(tenantId); + if (current != null && current.getTxId().equals(txId)) { + doAbortCurrentCommit(tenantId, current); + } + } else { + PendingCommit current = pendingCommitMap.get(tenantId); + if (current != null && current.getTxId().equals(txId)) { + try { + if (commitRequest.hasAddMsg()) { + addToCommit(ctx, current, commitRequest.getAddMsg()); + } else if (commitRequest.hasDeleteMsg()) { + deleteFromCommit(ctx, current, commitRequest.getDeleteMsg()); + } else if (commitRequest.hasPushMsg()) { + reply(ctx, vcService.push(current)); + } + } catch (Exception e) { + doAbortCurrentCommit(tenantId, current, e); + throw e; + } + } else { + log.debug("[{}] Ignore request due to stale commit: {}", txId, commitRequest); + } + } + } + + private void prepareCommit(VersionControlRequestCtx ctx, UUID txId, PrepareMsg prepareMsg) { + var tenantId = ctx.getTenantId(); + var pendingCommit = new PendingCommit(tenantId, ctx.getNodeId(), txId, prepareMsg.getBranchName(), prepareMsg.getCommitMsg()); + PendingCommit old = pendingCommitMap.get(tenantId); + if (old != null) { + doAbortCurrentCommit(tenantId, old); + } + pendingCommitMap.put(tenantId, pendingCommit); + vcService.prepareCommit(pendingCommit); + } + + private void deleteFromCommit(VersionControlRequestCtx ctx, PendingCommit commit, DeleteMsg deleteMsg) throws IOException { + vcService.deleteFolderContent(commit, deleteMsg.getRelativePath()); + } + + private void addToCommit(VersionControlRequestCtx ctx, PendingCommit commit, AddMsg addMsg) throws IOException { + vcService.add(commit, addMsg.getRelativePath(), addMsg.getEntityDataJson()); + } + + private void doAbortCurrentCommit(TenantId tenantId, PendingCommit current) { + doAbortCurrentCommit(tenantId, current, null); + } + + private void doAbortCurrentCommit(TenantId tenantId, PendingCommit current, Exception e) { + vcService.abort(current); + pendingCommitMap.remove(tenantId); + //TODO: push notification to core using old.getNodeId() to cancel old commit processing on the caller side. + } + private void handleClearRepositoryCommand(VersionControlRequestCtx ctx) { try { vcService.clearRepository(ctx.getTenantId()); @@ -191,13 +263,29 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe } } + private void reply(VersionControlRequestCtx ctx, VersionCreationResult result) { + reply(ctx, Optional.empty(), builder -> builder.setCommitResponse(CommitResponseMsg.newBuilder() + .setCommitId(result.getVersion().getId()) + .setName(result.getVersion().getName()) + .setAdded(result.getAdded()) + .setModified(result.getModified()) + .setRemoved(result.getRemoved()))); + } + private void reply(VersionControlRequestCtx ctx, Optional e) { + reply(ctx, e, null); + } + + private void reply(VersionControlRequestCtx ctx, Optional e, Function enrichFunction) { TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, ctx.getNodeId()); - TransportProtos.VersionControlResponseMsg.Builder builder = TransportProtos.VersionControlResponseMsg.newBuilder() + VersionControlResponseMsg.Builder builder = VersionControlResponseMsg.newBuilder() .setRequestIdMSB(ctx.getRequestId().getMostSignificantBits()) .setRequestIdLSB(ctx.getRequestId().getLeastSignificantBits()); if (e.isPresent()) { builder.setError(e.get().getMessage()); + } + if (enrichFunction != null) { + builder = enrichFunction.apply(builder); } else { builder.setGenericResponse(TransportProtos.GenericRepositoryResponseMsg.newBuilder().build()); } @@ -216,4 +304,9 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe } } + private Lock getRepoLock(TenantId tenantId) { + return tenantRepoLocks.computeIfAbsent(tenantId, t -> new ReentrantLock(true)); + } + + } diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java index 006a161ad8..0d55783a31 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.sync.vc; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; @@ -55,7 +54,10 @@ import java.util.stream.Collectors; @Service public class DefaultGitRepositoryService implements GitRepositoryService { - @Value("${vc.git.repos-poll-interval:${java.io.tmpdir}/repositories}") + @Value("${java.io.tmpdir}/repositories") + private String defaultFolder; + + @Value("${vc.git.folder:${java.io.tmpdir}/repositories}") private String repositoriesFolder; @Value("${vc.git.repos-poll-interval:60}") @@ -66,6 +68,9 @@ public class DefaultGitRepositoryService implements GitRepositoryService { @PostConstruct public void init() { + if (StringUtils.isEmpty(repositoriesFolder)) { + repositoriesFolder = defaultFolder; + } scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleWithFixedDelay(() -> { repositories.forEach((tenantId, repository) -> { @@ -89,7 +94,7 @@ public class DefaultGitRepositoryService implements GitRepositoryService { @Override public void prepareCommit(PendingCommit commit) { GitRepository repository = checkRepository(commit.getTenantId()); - String branch = commit.getRequest().getBranch(); + String branch = commit.getBranch(); try { repository.fetch(); if (repository.listBranches().contains(branch)) { @@ -139,7 +144,7 @@ public class DefaultGitRepositoryService implements GitRepositoryService { result.setModified(status.getModified().size()); result.setRemoved(status.getRemoved().size()); - GitRepository.Commit gitCommit = repository.commit(commit.getRequest().getVersionName()); + GitRepository.Commit gitCommit = repository.commit(commit.getVersionName()); repository.push(); result.setVersion(toVersion(gitCommit)); @@ -157,8 +162,8 @@ public class DefaultGitRepositoryService implements GitRepositoryService { @Override public String getFileContentAtCommit(TenantId tenantId, String relativePath, String versionId) throws IOException { - GitRepository repository = checkRepository(tenantId); - return repository.getFileContentAtCommit(relativePath, versionId); + GitRepository repository = checkRepository(tenantId); + return repository.getFileContentAtCommit(relativePath, versionId); } @Override @@ -213,6 +218,7 @@ public class DefaultGitRepositoryService implements GitRepositoryService { @Override public void initRepository(TenantId tenantId, EntitiesVersionControlSettings settings) throws Exception { + clearRepository(tenantId); Path repositoryDirectory = Path.of(repositoriesFolder, tenantId.getId().toString()); GitRepository repository; if (Files.exists(repositoryDirectory)) { @@ -224,6 +230,12 @@ public class DefaultGitRepositoryService implements GitRepositoryService { repositories.put(tenantId, repository); } + @Override + public EntitiesVersionControlSettings getRepositorySettings(TenantId tenantId) throws Exception { + var gitRepository = repositories.get(tenantId); + return gitRepository != null ? gitRepository.getSettings() : null; + } + @Override public void clearRepository(TenantId tenantId) throws IOException { GitRepository repository = repositories.get(tenantId); diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java index fa4891d35a..2b83112dfa 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java @@ -56,14 +56,17 @@ import java.util.stream.Collectors; public class GitRepository { private final Git git; + @Getter + private final EntitiesVersionControlSettings settings; private final CredentialsProvider credentialsProvider; private final SshdSessionFactory sshSessionFactory; @Getter private final String directory; - private GitRepository(Git git, CredentialsProvider credentialsProvider, SshdSessionFactory sshSessionFactory, String directory) { + private GitRepository(Git git, EntitiesVersionControlSettings settings, CredentialsProvider credentialsProvider, SshdSessionFactory sshSessionFactory, String directory) { this.git = git; + this.settings = settings; this.credentialsProvider = credentialsProvider; this.sshSessionFactory = sshSessionFactory; this.directory = directory; @@ -83,7 +86,7 @@ public class GitRepository { .setNoCheckout(true); configureTransportCommand(cloneCommand, credentialsProvider, sshSessionFactory); Git git = cloneCommand.call(); - return new GitRepository(git, credentialsProvider, sshSessionFactory, directory.getAbsolutePath()); + return new GitRepository(git, settings, credentialsProvider, sshSessionFactory, directory.getAbsolutePath()); } public static GitRepository open(File directory, EntitiesVersionControlSettings settings) throws IOException { @@ -95,7 +98,7 @@ public class GitRepository { } else if (VersionControlAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) { sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory); } - return new GitRepository(git, credentialsProvider, sshSessionFactory, directory.getAbsolutePath()); + return new GitRepository(git, settings, credentialsProvider, sshSessionFactory, directory.getAbsolutePath()); } public static void test(EntitiesVersionControlSettings settings, File directory) throws GitAPIException { diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepositoryService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepositoryService.java index 19754750c2..c748147856 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepositoryService.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepositoryService.java @@ -36,6 +36,8 @@ public interface GitRepositoryService { void initRepository(TenantId tenantId, EntitiesVersionControlSettings settings) throws Exception; + EntitiesVersionControlSettings getRepositorySettings(TenantId tenantId) throws Exception; + void clearRepository(TenantId tenantId) throws IOException; void add(PendingCommit commit, String relativePath, String entityDataJson) throws IOException; diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/PendingCommit.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/PendingCommit.java index a30f7514a2..ab54d9a066 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/PendingCommit.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/PendingCommit.java @@ -25,12 +25,16 @@ import java.util.UUID; public class PendingCommit { private final UUID txId; + private final String nodeId; private final TenantId tenantId; - private final VersionCreateRequest request; + private String branch; + private String versionName; - public PendingCommit(TenantId tenantId, VersionCreateRequest request) { - this.txId = UUID.randomUUID(); + public PendingCommit(TenantId tenantId, String nodeId, UUID txId, String branch, String versionName) { this.tenantId = tenantId; - this.request = request; + this.nodeId = nodeId; + this.txId = txId; + this.branch = branch; + this.versionName = versionName; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java index 7257cfb57b..de4a2fd5ed 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.DaoUtil; @@ -51,6 +52,7 @@ public class JpaAdminSettingsDao extends JpaAbstractDao { + return this.http.get(`/api/admin/vcSettings`, defaultHttpOptionsFromConfig(config)); + } + + public saveEntitiesVersionControlSettings(versionControlSettings: EntitiesVersionControlSettings, + config?: RequestConfig): Observable { + return this.http.post('/api/admin/vcSettings', versionControlSettings, + defaultHttpOptionsFromConfig(config)); + } + + public deleteEntitiesVersionControlSettings(config?: RequestConfig) { + return this.http.delete('/api/admin/vcSettings', defaultHttpOptionsFromConfig(config)); + } + + public checkVersionControlAccess(versionControlSettings: EntitiesVersionControlSettings, + config?: RequestConfig): Observable { + return this.http.post('/api/admin/vcSettings/checkAccess', versionControlSettings, defaultHttpOptionsFromConfig(config)); + } + public checkUpdates(config?: RequestConfig): Observable { return this.http.get(`/api/admin/updates`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/entities-version-control.service.ts b/ui-ngx/src/app/core/http/entities-version-control.service.ts new file mode 100644 index 0000000000..231581a02f --- /dev/null +++ b/ui-ngx/src/app/core/http/entities-version-control.service.ts @@ -0,0 +1,40 @@ +/// +/// 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 { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable } from 'rxjs'; +import { BranchInfo, VersionCreateRequest, VersionCreationResult } from '@shared/models/vc.models'; + +@Injectable({ + providedIn: 'root' +}) +export class EntitiesVersionControlService { + + constructor( + private http: HttpClient + ) { + } + + public listBranches(config?: RequestConfig): Observable> { + return this.http.get>('/api/entities/vc/branches', defaultHttpOptionsFromConfig(config)); + } + + public saveEntitiesVersion(request: VersionCreateRequest, config?: RequestConfig): Observable { + return this.http.post('/api/entities/vc/version', request, defaultHttpOptionsFromConfig(config)); + } +} diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 215693f318..56191ec948 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -362,7 +362,7 @@ export class MenuService { name: 'admin.system-settings', type: 'toggle', path: '/settings', - height: '80px', + height: '120px', icon: 'settings', pages: [ { @@ -378,6 +378,13 @@ export class MenuService { type: 'link', path: '/settings/resources-library', icon: 'folder' + }, + { + id: guid(), + name: 'admin.git-settings', + type: 'link', + path: '/settings/vc', + icon: 'manage_history' } ] } @@ -512,6 +519,11 @@ export class MenuService { name: 'resource.resources-library', icon: 'folder', path: '/settings/resources-library' + }, + { + name: 'admin.git-settings', + icon: 'manage_history', + path: '/settings/vc', } ] } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 5b59bb0938..05067e78ee 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -153,6 +153,7 @@ import { TenantProfileQueuesComponent } from '@home/components/profile/queue/ten import { QueueFormComponent } from '@home/components/queue/queue-form.component'; import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module'; import { WidgetSettingsComponent } from '@home/components/widget/widget-settings.component'; +import { VcEntityExportDialogComponent } from '@home/components/vc/vc-entity-export-dialog.component'; @NgModule({ declarations: @@ -276,7 +277,8 @@ import { WidgetSettingsComponent } from '@home/components/widget/widget-settings EmbedDashboardDialogComponent, DisplayWidgetTypesPanelComponent, TenantProfileQueuesComponent, - QueueFormComponent + QueueFormComponent, + VcEntityExportDialogComponent ], imports: [ CommonModule, @@ -394,7 +396,8 @@ import { WidgetSettingsComponent } from '@home/components/widget/widget-settings EmbedDashboardDialogComponent, DisplayWidgetTypesPanelComponent, TenantProfileQueuesComponent, - QueueFormComponent + QueueFormComponent, + VcEntityExportDialogComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html index f5b9c15a0f..57286fd7ee 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html @@ -34,6 +34,12 @@ [fxShow]="!isEdit && !entity?.default"> {{'device-profile.set-default' | translate }} + + + + +
+
+
+
+ + + + version-control.version-name + + + {{ 'version-control.version-name-required' | translate }} + + + + {{ 'version-control.export-entity-relations' | translate }} + +
+
+
+
+
+
+
+
+ + +
+
+ +
+ diff --git a/ui-ngx/src/app/modules/home/components/vc/vc-entity-export-dialog.component.ts b/ui-ngx/src/app/modules/home/components/vc/vc-entity-export-dialog.component.ts new file mode 100644 index 0000000000..9d3affea42 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/vc/vc-entity-export-dialog.component.ts @@ -0,0 +1,105 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { EntityId } from '@shared/models/id/entity-id'; +import { + SingleEntityVersionCreateRequest, + VersionCreateRequestType, + VersionCreationResult +} from '@shared/models/vc.models'; +import { EntitiesVersionControlService } from '@core/http/entities-version-control.service'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { TranslateService } from '@ngx-translate/core'; + +export interface VcEntityExportDialogData { + entityId: EntityId; +} + +@Component({ + selector: 'tb-vc-entity-export-dialog', + templateUrl: './vc-entity-export-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: VcEntityExportDialogComponent}], + styleUrls: [] +}) +export class VcEntityExportDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + exportFormGroup: FormGroup; + + submitted = false; + + createResult: VersionCreationResult; + + createResultMessage: SafeHtml; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: VcEntityExportDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private entitiesVersionControlService: EntitiesVersionControlService, + private translate: TranslateService, + private domSanitizer: DomSanitizer, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.exportFormGroup = this.fb.group({ + branch: [null, [Validators.required]], + versionName: [null, [Validators.required]], + saveRelations: [false, []] + }); + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(); + } + + export(): void { + this.submitted = true; + const request: SingleEntityVersionCreateRequest = { + entityId: this.data.entityId, + branch: this.exportFormGroup.get('branch').value, + versionName: this.exportFormGroup.get('versionName').value, + config: { + saveRelations: this.exportFormGroup.get('saveRelations').value + }, + type: VersionCreateRequestType.SINGLE_ENTITY + }; + this.entitiesVersionControlService.saveEntitiesVersion(request).subscribe((result) => { + this.createResult = result; + const message = this.translate.instant('version-control.export-entity-version-result-message', + {name: result.version.name, commitId: result.version.id}); + this.createResultMessage = this.domSanitizer.bypassSecurityTrustHtml(message); + }); + } +} diff --git a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts index 6dc8746b2e..1730c69084 100644 --- a/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts +++ b/ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts @@ -22,6 +22,11 @@ import { ImportDialogCsvComponent, ImportDialogCsvData } from '@home/components/import-export/import-dialog-csv.component'; +import { EntityId } from '@shared/models/id/entity-id'; +import { + VcEntityExportDialogComponent, + VcEntityExportDialogData +} from '@home/components/vc/vc-entity-export-dialog.component'; @Injectable() export class HomeDialogsService { @@ -41,6 +46,17 @@ export class HomeDialogsService { } } + public exportVcEntity(entityId: EntityId): Observable { + return this.dialog.open(VcEntityExportDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entityId + } + }).afterClosed(); + } + private openImportDialogCSV(entityType: EntityType, importTitle: string, importFileLabel: string): Observable { return this.dialog.open(ImportDialogCsvComponent, { @@ -53,4 +69,5 @@ export class HomeDialogsService { } }).afterClosed(); } + } diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index e5252f915f..d95b55e5f4 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -33,6 +33,7 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { QueuesTableConfigResolver } from '@home/pages/admin/queue/queues-table-config.resolver'; +import { VersionControlSettingsComponent } from '@home/pages/admin/version-control-settings.component'; @Injectable() export class OAuth2LoginProcessingUrlResolver implements Resolve { @@ -222,6 +223,19 @@ const routes: Routes = [ } } ] + }, + { + path: 'vc', + component: VersionControlSettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.TENANT_ADMIN], + title: 'admin.git-settings', + breadcrumb: { + label: 'admin.git-settings', + icon: 'manage_history' + } + } } ] } diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index 6f5e91bd61..aab8d4d56f 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -29,6 +29,7 @@ import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dial import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component'; import { QueueComponent} from '@home/pages/admin/queue/queue.component'; +import { VersionControlSettingsComponent } from '@home/pages/admin/version-control-settings.component'; @NgModule({ declarations: @@ -41,7 +42,8 @@ import { QueueComponent} from '@home/pages/admin/queue/queue.component'; OAuth2SettingsComponent, HomeSettingsComponent, ResourcesLibraryComponent, - QueueComponent + QueueComponent, + VersionControlSettingsComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.html new file mode 100644 index 0000000000..b512bb96f5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.html @@ -0,0 +1,110 @@ + +
+ + +
+ admin.git-repository-settings + +
+
+
+ + +
+ +
+
+ + admin.repository-url + + + admin.repository-url-required + + + + admin.default-branch + + +
+ admin.authentication-settings + + admin.auth-method + + + {{versionControlAuthMethodTranslations.get(method) | translate}} + + + +
+ + common.username + + + + {{ 'admin.change-password-access-token' | translate }} + + + admin.password-access-token + + + +
+
+ + + + {{ 'admin.change-passphrase' | translate }} + + + admin.passphrase + + + +
+
+
+ + + + +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.scss new file mode 100644 index 0000000000..ede3570e68 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.scss @@ -0,0 +1,33 @@ +/** + * 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 { + .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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.ts new file mode 100644 index 0000000000..c2626ec062 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/version-control-settings.component.ts @@ -0,0 +1,198 @@ +/// +/// 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 { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; +import { 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 { + 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'; + +@Component({ + selector: 'tb-version-control-settings', + templateUrl: './version-control-settings.component.html', + styleUrls: ['./version-control-settings.component.scss', './settings-card.scss'] +}) +export class VersionControlSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { + + 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, + 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: [null, []], + 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.adminService.getEntitiesVersionControlSettings({ignoreErrors: true}).subscribe( + (settings) => { + this.settings = settings; + 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); + } + ); + } + + 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({ authMethod: VersionControlAuthMethod.USERNAME_PASSWORD }); + this.updateValidators(false); + } + ); + } + }); + } + + confirmForm(): FormGroup { + return this.versionControlSettingsForm; + } + + 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}); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset.component.html b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html index 00ed882cd9..75171b4532 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset.component.html +++ b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html @@ -46,6 +46,12 @@ [fxShow]="!isEdit && assetScope === 'edge'"> {{ 'edge.unassign-from-edge' | translate }} + + + + + + + + + + + + {{ 'version-control.branch-required' | translate }} + + diff --git a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts new file mode 100644 index 0000000000..421710181a --- /dev/null +++ b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts @@ -0,0 +1,199 @@ +/// +/// 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 { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { + catchError, + debounceTime, + distinctUntilChanged, + map, + publishReplay, + refCount, + switchMap, + tap +} from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { BranchInfo } from '@shared/models/vc.models'; +import { EntitiesVersionControlService } from '@core/http/entities-version-control.service'; + +@Component({ + selector: 'tb-branch-autocomplete', + templateUrl: './branch-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => BranchAutocompleteComponent), + multi: true + }] +}) +export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit { + + branchFormGroup: FormGroup; + + modelValue: string | null; + + private requiredValue: boolean; + + get required(): boolean { + return this.requiredValue; + } + + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @Input() + selectDefaultBranch = true; + + @ViewChild('branchInput', {static: true}) branchInput: ElementRef; + + filteredBranches: Observable>; + + branches: Observable>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private entitiesVersionControlService: EntitiesVersionControlService, + private fb: FormBuilder) { + this.branchFormGroup = this.fb.group({ + branch: [null, []] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + + this.branches = null; + this.filteredBranches = this.branchFormGroup.get('branch').valueChanges + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap(value => { + this.updateView(value); + }), + map(value => value ? value : ''), + switchMap(branch => this.fetchBranches(branch)) + ); + } + + ngAfterViewInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.branchFormGroup.disable({emitEvent: false}); + } else { + this.branchFormGroup.enable({emitEvent: false}); + } + } + + selectDefaultBranchIfNeeded(): void { + if (this.selectDefaultBranch && !this.modelValue) { + this.getBranches().subscribe( + (data) => { + if (data && data.length) { + const defaultBranch = data.find(branch => branch.default); + if (defaultBranch) { + this.modelValue = defaultBranch.name; + this.branchFormGroup.get('branch').patchValue(this.modelValue, {emitEvent: false}); + this.propagateChange(this.modelValue); + } + } + } + ); + } + } + + writeValue(value: string | null): void { + this.searchText = ''; + this.modelValue = value; + if (value != null) { + this.branchFormGroup.get('branch').patchValue(value, {emitEvent: false}); + } else { + this.branchFormGroup.get('branch').patchValue('', {emitEvent: false}); + this.selectDefaultBranchIfNeeded(); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.branchFormGroup.get('branch').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayBranchFn(branch?: string): string | undefined { + return branch ? branch : undefined; + } + + fetchBranches(searchText?: string): Observable> { + this.searchText = searchText; + return this.getBranches().pipe( + map(branches => branches.map(branch => branch.name).filter(branchName => { + return searchText ? branchName.toUpperCase().startsWith(searchText.toUpperCase()) : true; + })) + ); + } + + getBranches(): Observable> { + if (!this.branches) { + const branchesObservable = this.entitiesVersionControlService.listBranches({ignoreLoading: true, ignoreErrors: true}); + this.branches = branchesObservable.pipe( + catchError(() => of([] as Array)), + publishReplay(1), + refCount() + ); + } + return this.branches; + } + + clear() { + this.branchFormGroup.get('branch').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.branchInput.nativeElement.blur(); + this.branchInput.nativeElement.focus(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 29f6ccc020..6297809522 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -134,7 +134,8 @@ export const HelpLinks = { widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static', 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', - queue: helpBaseUrl + '/docs/user-guide/queue' + queue: helpBaseUrl + '/docs/user-guide/queue', + versionControlSettings: helpBaseUrl + '/docs/user-guide/ui/version-control-settings' } }; diff --git a/ui-ngx/src/app/shared/models/settings.models.ts b/ui-ngx/src/app/shared/models/settings.models.ts index 615d0994c9..8a1b316001 100644 --- a/ui-ngx/src/app/shared/models/settings.models.ts +++ b/ui-ngx/src/app/shared/models/settings.models.ts @@ -396,3 +396,24 @@ export function createSmsProviderConfiguration(type: SmsProviderType): SmsProvid } return smsProviderConfiguration; } + +export enum VersionControlAuthMethod { + USERNAME_PASSWORD = 'USERNAME_PASSWORD', + PRIVATE_KEY = 'PRIVATE_KEY' +} + +export const versionControlAuthMethodTranslationMap = new Map([ + [VersionControlAuthMethod.USERNAME_PASSWORD, 'admin.auth-method-username-password'], + [VersionControlAuthMethod.PRIVATE_KEY, 'admin.auth-method-private-key'] +]); + +export interface EntitiesVersionControlSettings { + repositoryUri: string; + defaultBranch: string; + authMethod: VersionControlAuthMethod; + username: string; + password: string; + privateKeyFileName: string; + privateKey: string; + privateKeyPassword: string; +} diff --git a/ui-ngx/src/app/shared/models/vc.models.ts b/ui-ngx/src/app/shared/models/vc.models.ts new file mode 100644 index 0000000000..0c890ff9b4 --- /dev/null +++ b/ui-ngx/src/app/shared/models/vc.models.ts @@ -0,0 +1,55 @@ +/// +/// 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 { EntityId } from '@shared/models/id/entity-id'; + +export interface VersionCreateConfig { + saveRelations: boolean; +} + +export enum VersionCreateRequestType { + SINGLE_ENTITY = 'SINGLE_ENTITY', + COMPLEX = 'COMPLEX' +} + +export interface VersionCreateRequest { + versionName: string; + branch: string; + type: VersionCreateRequestType; +} + +export interface SingleEntityVersionCreateRequest extends VersionCreateRequest { + entityId: EntityId; + config: VersionCreateConfig; + type: VersionCreateRequestType.SINGLE_ENTITY; +} + +export interface BranchInfo { + name: string; + default: boolean; +} + +export interface EntityVersion { + id: string; + name: string; +} + +export interface VersionCreationResult { + version: EntityVersion; + added: number; + modified: number; + removed: number; +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 287e8e21b6..ca302fc06f 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -163,6 +163,7 @@ import { HtmlComponent } from '@shared/components/html.component'; import { SafePipe } from '@shared/pipe/safe.pipe'; import { DragDropModule } from '@angular/cdk/drag-drop'; import { MultipleImageInputComponent } from '@shared/components/multiple-image-input.component'; +import { BranchAutocompleteComponent } from '@shared/components/vc/branch-autocomplete.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -284,7 +285,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) WidgetsBundleSearchComponent, CopyButtonComponent, TogglePasswordComponent, - ProtobufContentComponent + ProtobufContentComponent, + BranchAutocompleteComponent ], imports: [ CommonModule, @@ -484,7 +486,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) WidgetsBundleSearchComponent, CopyButtonComponent, TogglePasswordComponent, - ProtobufContentComponent + ProtobufContentComponent, + BranchAutocompleteComponent ] }) export class SharedModule { } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 93e1afe006..c37ba685e0 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -319,8 +319,28 @@ "queue-partitions": "Partitions", "queue-submit-strategy": "Submit strategy", "queue-processing-strategy": "Processing strategy", - "queue-configuration": "Queue configuration" - }, + "queue-configuration": "Queue configuration", + "git-settings": "Git settings", + "git-repository-settings": "Git repository settings", + "repository-url": "Repository URL", + "repository-url-required": "Repository URL is required.", + "default-branch": "Default branch name", + "authentication-settings": "Authentication settings", + "auth-method": "Authentication method", + "auth-method-username-password": "Password / access token", + "auth-method-private-key": "Private key", + "password-access-token": "Password / access token", + "change-password-access-token": "Change password / access token", + "private-key": "Private key", + "drop-private-key-file-or": "Drag and drop a private key file or", + "passphrase": "Passphrase", + "enter-passphrase": "Enter passphrase", + "change-passphrase": "Change passphrase", + "check-access": "Check access", + "check-vc-access-success": "Git repository access successfully verified!", + "delete-git-settings-title": "Are you sure you want to delete git settings?", + "delete-git-settings-text": "Be careful, after the confirmation the git settings will be removed and git synchronization feature will be unavailable." + }, "alarm": { "alarm": "Alarm", "alarms": "Alarms", @@ -3088,6 +3108,18 @@ "json-value-invalid": "JSON value has an invalid format", "json-value-required": "JSON value is required." }, + "version-control": { + "branch": "Branch", + "select-branch": "Select branch", + "branch-required": "Branch is required", + "export-entity-version": "Export entity version", + "entity-version-exported": "Entity version successfully exported", + "version-name": "Version name", + "version-name-required": "Version name is required", + "export-entity-relations": "Export entity relations", + "export-entity-version-result-message": "Entity exported with version '{{name}}' and commit id '{{commitId}}'.", + "export-to-git": "Export to Git" + }, "widget": { "widget-library": "Widgets Library", "widget-bundle": "Widgets Bundle",