Browse Source

add revoke item feature

add revoke item feature

Co-authored-by: zhuohao.li <zhuohao.li@daocloud.io>
Zhuohao Li 5 years ago
parent
commit
9f4bac4fc7

+ 6 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ItemController.java

@@ -208,6 +208,12 @@ public class ItemController {
     return ResponseEntity.ok().build();
   }
 
+  @PreAuthorize(value = "@permissionValidator.hasModifyNamespacePermission(#appId, #namespaceName, #env)")
+  @PutMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/revoke-items")
+  public void revokeItems(@PathVariable String appId, @PathVariable String env, @PathVariable String clusterName,
+      @PathVariable String namespaceName) {
+    configService.revokeItem(appId, Env.valueOf(env), clusterName, namespaceName);
+  }
   private void doSyntaxCheck(NamespaceTextModel model) {
     if (StringUtils.isBlank(model.getConfigText())) {
       return;

+ 70 - 2
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ItemService.java

@@ -1,12 +1,17 @@
 package com.ctrip.framework.apollo.portal.service;
 
 
+import com.ctrip.framework.apollo.common.constants.GsonType;
 import com.ctrip.framework.apollo.common.dto.ItemChangeSets;
 import com.ctrip.framework.apollo.common.dto.ItemDTO;
 import com.ctrip.framework.apollo.common.dto.NamespaceDTO;
+import com.ctrip.framework.apollo.common.dto.ReleaseDTO;
 import com.ctrip.framework.apollo.common.exception.BadRequestException;
 import com.ctrip.framework.apollo.common.utils.BeanUtils;
 import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
+import com.ctrip.framework.apollo.portal.api.AdminServiceAPI.ItemAPI;
+import com.ctrip.framework.apollo.portal.api.AdminServiceAPI.NamespaceAPI;
+import com.ctrip.framework.apollo.portal.api.AdminServiceAPI.ReleaseAPI;
 import com.ctrip.framework.apollo.portal.environment.Env;
 import com.ctrip.framework.apollo.core.utils.StringUtils;
 import com.ctrip.framework.apollo.portal.api.AdminServiceAPI;
@@ -17,6 +22,9 @@ import com.ctrip.framework.apollo.portal.entity.vo.ItemDiffs;
 import com.ctrip.framework.apollo.portal.entity.vo.NamespaceIdentifier;
 import com.ctrip.framework.apollo.portal.spi.UserInfoHolder;
 import com.ctrip.framework.apollo.tracer.Tracer;
+import com.google.gson.Gson;
+import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicInteger;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.http.HttpStatus;
 import org.springframework.stereotype.Service;
@@ -29,22 +37,26 @@ import java.util.Map;
 
 @Service
 public class ItemService {
+  private Gson gson = new Gson();
 
   private final UserInfoHolder userInfoHolder;
   private final AdminServiceAPI.NamespaceAPI namespaceAPI;
   private final AdminServiceAPI.ItemAPI itemAPI;
+  private final AdminServiceAPI.ReleaseAPI releaseAPI;
   private final ConfigTextResolver fileTextResolver;
   private final ConfigTextResolver propertyResolver;
 
   public ItemService(
       final UserInfoHolder userInfoHolder,
-      final AdminServiceAPI.NamespaceAPI namespaceAPI,
-      final AdminServiceAPI.ItemAPI itemAPI,
+      final NamespaceAPI namespaceAPI,
+      final ItemAPI itemAPI,
+      final ReleaseAPI releaseAPI,
       final @Qualifier("fileTextResolver") ConfigTextResolver fileTextResolver,
       final @Qualifier("propertyResolver") ConfigTextResolver propertyResolver) {
     this.userInfoHolder = userInfoHolder;
     this.namespaceAPI = namespaceAPI;
     this.itemAPI = itemAPI;
+    this.releaseAPI = releaseAPI;
     this.fileTextResolver = fileTextResolver;
     this.propertyResolver = propertyResolver;
   }
@@ -144,6 +156,55 @@ public class ItemService {
     }
   }
 
+
+  public void revokeItem(String appId, Env env, String clusterName, String namespaceName) {
+
+    NamespaceDTO namespace = namespaceAPI.loadNamespace(appId, env, clusterName, namespaceName);
+    if (namespace == null) {
+      throw new BadRequestException(
+          "namespace:" + namespaceName + " not exist in env:" + env + ", cluster:" + clusterName);
+    }
+    long namespaceId = namespace.getId();
+
+    Map<String, String> releaseItemDTOs = new HashMap<>();
+    ReleaseDTO latestRelease = releaseAPI.loadLatestRelease(appId,env,clusterName,namespaceName);
+    if (latestRelease != null) {
+      releaseItemDTOs = gson.fromJson(latestRelease.getConfigurations(), GsonType.CONFIG);
+    }
+    List<ItemDTO> baseItems = itemAPI.findItems(appId, env, clusterName, namespaceName);
+    Map<String, ItemDTO> oldKeyMapItem = BeanUtils.mapByKey("key", baseItems);
+    Map<String, ItemDTO> deletedItemDTOs = new HashMap<>();
+
+    //deleted items for comment
+    findDeletedItems(appId, env, clusterName, namespaceName).forEach(item -> {
+      deletedItemDTOs.put(item.getKey(),item);
+    });
+
+    ItemChangeSets changeSets = new ItemChangeSets();
+    AtomicInteger lineNum = new AtomicInteger(1);
+    releaseItemDTOs.forEach((key,value) -> {
+      ItemDTO oldItem = oldKeyMapItem.get(key);
+      if (oldItem == null) {
+        ItemDTO deletedItemDto = deletedItemDTOs.computeIfAbsent(key, k -> new ItemDTO());
+        changeSets.addCreateItem(buildNormalItem(0L, namespaceId,key,value,deletedItemDto.getComment(),lineNum.get()));
+      } else if (!oldItem.getValue().equals(value) || lineNum.get() != oldItem
+          .getLineNum()) {
+        changeSets.addUpdateItem(buildNormalItem(oldItem.getId(), namespaceId, key,
+            value, oldItem.getComment(), lineNum.get()));
+      }
+      oldKeyMapItem.remove(key);
+      lineNum.set(lineNum.get() + 1);
+    });
+    oldKeyMapItem.forEach((key, value) -> changeSets.addDeleteItem(oldKeyMapItem.get(key)));
+    changeSets.setDataChangeLastModifiedBy(userInfoHolder.getUser().getUserId());
+
+    updateItems(appId, env, clusterName, namespaceName, changeSets);
+
+    Tracer.logEvent(TracerEventType.MODIFY_NAMESPACE_BY_TEXT,
+        String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName));
+    Tracer.logEvent(TracerEventType.MODIFY_NAMESPACE, String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName));
+  }
+
   public List<ItemDiffs> compare(List<NamespaceIdentifier> comparedNamespaces, List<ItemDTO> sourceItems) {
 
     List<ItemDiffs> result = new LinkedList<>();
@@ -231,6 +292,13 @@ public class ItemService {
     return createdItem;
   }
 
+  private ItemDTO buildNormalItem(Long id, Long namespaceId, String key, String value, String comment, int lineNum) {
+    ItemDTO item = new ItemDTO(key, value, comment, lineNum);
+    item.setId(id);
+    item.setNamespaceId(namespaceId);
+    return item;
+  }
+
   private boolean isModified(String sourceValue, String targetValue, String sourceComment, String targetComment) {
 
     if (!sourceValue.equals(targetValue)) {

+ 9 - 1
apollo-portal/src/main/resources/static/config.html

@@ -208,7 +208,8 @@
             <apollonspanel ng-repeat="namespace in namespaces" namespace="namespace" app-id="pageContext.appId"
                 env="pageContext.env" lock-check="lockCheck" cluster="pageContext.clusterName" user="currentUser"
                 pre-release-ns="prepareReleaseNamespace" create-item="createItem" edit-item="editItem"
-                pre-delete-item="preDeleteItem" show-text="showText"
+                pre-delete-item="preDeleteItem" pre-revoke-item="preRevokeItem"
+                show-text="showText"
                 show-no-modify-permission-dialog="showNoModifyPermissionDialog" show-body="namespaces.length < 3"
                 lazy-load="namespaces.length > 10" pre-create-branch="preCreateBranch"
                 pre-delete-branch="preDeleteBranch">
@@ -318,6 +319,12 @@
                 apollo-detail="syntaxCheckContext.syntaxCheckMessage" apollo-extra-class="'pre'">
             </apolloconfirmdialog>
 
+            <apolloconfirmdialog apollo-dialog-id="'revokeItemConfirmDialog'"
+                apollo-title="'Config.RevokeItem.DialogTitle' | translate"
+                apollo-detail="'Config.RevokeItem.DialogContent' | translate:this" apollo-show-cancel-btn="true"
+                apollo-confirm="revokeItem">
+           </apolloconfirmdialog>
+
 
             <div class="modal fade" id="createBranchTips" tabindex="-1" role="dialog">
                 <div class="modal-dialog" role="document">
@@ -338,6 +345,7 @@
                     </div>
                 </div>
             </div>
+
         </div>
     </div>
 

+ 7 - 1
apollo-portal/src/main/resources/static/i18n/en.json

@@ -185,6 +185,8 @@
   "Component.Namespace.Master.Items.SummitChanged": "Submit",
   "Component.Namespace.Master.Items.SortByKey": "Filter the configurations by key",
   "Component.Namespace.Master.Items.FilterItem": "Filter",
+  "Component.Namespace.Master.Items.RevokeItemTips": "Revoke configuration changes",
+  "Component.Namespace.Master.Items.RevokeItem" :"Revoke",
   "Component.Namespace.Master.Items.SyncItemTips": "Synchronize configurations among environments",
   "Component.Namespace.Master.Items.SyncItem": "Synchronize",
   "Component.Namespace.Master.Items.DiffItemTips": "Compare the configurations among environments",
@@ -345,6 +347,8 @@
   "Config.ClusterIsDefaultTipContent": "All instances that do not belong to the '{{name}}' cluster will fetch the default cluster (current page) configuration, and those that belong to the '{{name}}' cluster will use the corresponding cluster configuration!",
   "Config.ClusterIsCustomTipContent": "Instances belonging to the '{{name}}' cluster will only fetch the configuration of the '{{name}}' cluster (the current page), and the default cluster configuration will only be fetched when the corresponding namespace has not been released in the current cluster.",
   "Config.HasNotPublishNamespace": "The following environment/cluster has unreleased configurations, the client will not fetch the unreleased configurations, please release them in time.",
+  "Config.RevokeItem.DialogTitle": "Revoke configuration changes",
+  "Config.RevokeItem.DialogContent": "Modified but unpublished configurations in the current namespace will be revoked. Are you sure to revoke the configuration changes?",
   "Config.DeleteItem.DialogTitle": "Delete configuration",
   "Config.DeleteItem.DialogContent": "You are deleting the configuration whose Key is <b>'{{config.key}}'</b> Value is <b>'{{config.value}}'</b>. <br> Are you sure to delete the configuration?",
   "Config.PublishNoPermission.DialogTitle": "Release",
@@ -746,5 +750,7 @@
   "ReleaseModal.AllPublishFailed": "Failed to Full Release",
   "Rollback.NoRollbackList": "No released history to rollback",
   "Rollback.RollbackSuccessfully": "Rollback Successfully",
-  "Rollback.RollbackFailed": "Failed to Rollback"
+  "Rollback.RollbackFailed": "Failed to Rollback",
+  "Revoke.RevokeFailed": "Failed to Revoke",
+  "Revoke.RevokeSuccessfully": "Revoke Successfully"
 }

+ 7 - 1
apollo-portal/src/main/resources/static/i18n/zh-CN.json

@@ -187,6 +187,8 @@
   "Component.Namespace.Master.Items.FilterItem": "过滤配置",
   "Component.Namespace.Master.Items.SyncItemTips": "同步各环境间配置",
   "Component.Namespace.Master.Items.SyncItem": "同步配置",
+  "Component.Namespace.Master.Items.RevokeItemTips": "撤销配置的修改",
+  "Component.Namespace.Master.Items.RevokeItem": "撤销配置",
   "Component.Namespace.Master.Items.DiffItemTips": "比较各环境间配置",
   "Component.Namespace.Master.Items.DiffItem": "比较配置",
   "Component.Namespace.Master.Items.AddItem": "新增配置",
@@ -345,6 +347,8 @@
   "Config.ClusterIsDefaultTipContent": "所有不属于 '{{name}}' 集群的实例会使用default集群(当前页面)的配置,属于 '{{name}}' 的实例会使用对应集群的配置!",
   "Config.ClusterIsCustomTipContent": "属于 '{{name}}' 集群的实例只会使用 '{{name}}' 集群(当前页面)的配置,只有当对应namespace在当前集群没有发布过配置时,才会使用default集群的配置。",
   "Config.HasNotPublishNamespace": "以下环境/集群有未发布的配置,客户端获取不到未发布的配置,请及时发布。",
+  "Config.RevokeItem.DialogTitle": "撤销配置",
+  "Config.RevokeItem.DialogContent": "当前命名空间下已修改但尚未发布的配置将被撤销,确定要撤销么?",
   "Config.DeleteItem.DialogTitle": "删除配置",
   "Config.DeleteItem.DialogContent": "您正在删除 Key 为 <b> '{{config.key}}' </b> Value 为 <b> '{{config.value}}' </b> 的配置.<br>确定要删除配置吗?",
   "Config.PublishNoPermission.DialogTitle": "发布",
@@ -746,5 +750,7 @@
   "ReleaseModal.AllPublishFailed": "全量发布失败",
   "Rollback.NoRollbackList": "没有可以回滚的发布历史",
   "Rollback.RollbackSuccessfully": "回滚成功",
-  "Rollback.RollbackFailed": "回滚失败"
+  "Rollback.RollbackFailed": "回滚失败",
+  "Revoke.RevokeFailed": "撤销失败",
+  "Revoke.RevokeSuccessfully": "撤销成功"
 }

+ 29 - 0
apollo-portal/src/main/resources/static/scripts/controller/config/ConfigNamespaceController.js

@@ -11,6 +11,8 @@ function controller($rootScope, $scope, $translate, toastr, AppUtil, EventManage
     $scope.deleteItem = deleteItem;
     $scope.editItem = editItem;
     $scope.createItem = createItem;
+    $scope.preRevokeItem = preRevokeItem;
+    $scope.revokeItem = revokeItem;
     $scope.closeTip = closeTip;
     $scope.showText = showText;
     $scope.createBranch = createBranch;
@@ -178,6 +180,33 @@ function controller($rootScope, $scope, $translate, toastr, AppUtil, EventManage
                 });
     }
 
+    function preRevokeItem(namespace) {
+        if (!lockCheck(namespace)) {
+            return;
+        }
+        $scope.toOperationNamespace = namespace;
+        toRevokeItemId = namespace.baseInfo.id;
+        $("#revokeItemConfirmDialog").modal("show");
+    }
+
+    function revokeItem() {
+        ConfigService.revoke_item($rootScope.pageContext.appId,
+            $rootScope.pageContext.env,
+            $scope.toOperationNamespace.baseInfo.clusterName,
+            $scope.toOperationNamespace.baseInfo.namespaceName).then(
+            function (result) {
+                toastr.success($translate.instant('Revoke.RevokeSuccessfully'));
+                EventManager.emit(EventManager.EventType.REFRESH_NAMESPACE,
+                    {
+                        namespace: $scope.toOperationNamespace
+                    });
+
+            }, function (result) {
+                toastr.error(AppUtil.errorMsg(result), $translate.instant('Revoke.RevokeFailed'));
+            }
+        );
+    }
+
     //修改配置
     function editItem(namespace, toEditItem) {
         if (!lockCheck(namespace)) {

+ 1 - 0
apollo-portal/src/main/resources/static/scripts/directive/namespace-panel-directive.js

@@ -17,6 +17,7 @@ function directive($window, $translate, toastr, AppUtil, EventManager, Permissio
             createItem: '=',
             editItem: '=',
             preDeleteItem: '=',
+            preRevokeItem: '=',
             showText: '=',
             showNoModifyPermissionDialog: '=',
             preCreateBranch: '=',

+ 21 - 1
apollo-portal/src/main/resources/static/scripts/services/ConfigService.js

@@ -49,7 +49,11 @@ appService.service("ConfigService", ['$resource', '$q', 'AppUtil', function ($re
         syntax_check_text: {
             method: 'POST',
             url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/syntax-check'
-        }
+        },
+        revoke_item: {
+            method: 'PUT',
+            url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/revoke-items'
+        },
     });
 
     return {
@@ -214,6 +218,22 @@ appService.service("ConfigService", ['$resource', '$q', 'AppUtil', function ($re
                     d.reject(result);
                 });
             return d.promise;
+        },
+
+        revoke_item:  function (appId, env, clusterName, namespaceName) {
+            var d = $q.defer();
+            config_source.revoke_item({
+                  appId: appId,
+                  env: env,
+                  clusterName: clusterName,
+                  namespaceName: namespaceName
+                },{}, function (result) {
+                  d.resolve(result);
+
+                }, function (result) {
+                  d.reject(result);
+                });
+            return d.promise;
         }
     }
 

+ 8 - 0
apollo-portal/src/main/resources/static/views/component/namespace-panel-master-tab.html

@@ -183,6 +183,14 @@
                         {{'Component.Namespace.Master.Items.SyncItem' | translate }}
                     </button>
 
+                    <button type="button" class="btn btn-default btn-sm J_tableview_btn" data-tooltip="tooltip"
+                            data-placement="bottom" title="{{'Component.Namespace.Master.Items.RevokeItemTips' | translate }}"
+                            ng-click="preRevokeItem(namespace)" ng-show="namespace.viewType == 'table' && namespace.displayControl.currentOperateBranch == 'master'
+                            && namespace.hasModifyPermission && namespace.isPropertiesFormat">
+                        <img src="img/rollback.png">
+                        {{'Component.Namespace.Master.Items.RevokeItem' | translate }}
+                    </button>
+
                     <button type="button" class="btn btn-default btn-sm J_tableview_btn" data-tooltip="tooltip"
                         data-placement="bottom" title="{{'Component.Namespace.Master.Items.DiffItemTips' | translate }}"
                         ng-click="goToDiffPage(namespace)" ng-show="namespace.viewType == 'table' && namespace.displayControl.currentOperateBranch == 'master'