Commit 9f4bac4f authored by Zhuohao Li's avatar Zhuohao Li Committed by GitHub

add revoke item feature

add revoke item feature
Co-authored-by: default avatarzhuohao.li <zhuohao.li@daocloud.io>
parent f3395f07
......@@ -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;
......
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)) {
......
......@@ -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>
......
......@@ -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"
}
\ No newline at end of file
......@@ -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": "撤销成功"
}
\ No newline at end of file
......@@ -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)) {
......
......@@ -17,6 +17,7 @@ function directive($window, $translate, toastr, AppUtil, EventManager, Permissio
createItem: '=',
editItem: '=',
preDeleteItem: '=',
preRevokeItem: '=',
showText: '=',
showNoModifyPermissionDialog: '=',
preCreateBranch: '=',
......
......@@ -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;
}
}
......
......@@ -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'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment