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 { ...@@ -208,6 +208,12 @@ public class ItemController {
return ResponseEntity.ok().build(); 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) { private void doSyntaxCheck(NamespaceTextModel model) {
if (StringUtils.isBlank(model.getConfigText())) { if (StringUtils.isBlank(model.getConfigText())) {
return; return;
......
package com.ctrip.framework.apollo.portal.service; 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.ItemChangeSets;
import com.ctrip.framework.apollo.common.dto.ItemDTO; import com.ctrip.framework.apollo.common.dto.ItemDTO;
import com.ctrip.framework.apollo.common.dto.NamespaceDTO; 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.exception.BadRequestException;
import com.ctrip.framework.apollo.common.utils.BeanUtils; import com.ctrip.framework.apollo.common.utils.BeanUtils;
import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; 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.portal.environment.Env;
import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.core.utils.StringUtils;
import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI;
...@@ -17,6 +22,9 @@ import com.ctrip.framework.apollo.portal.entity.vo.ItemDiffs; ...@@ -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.entity.vo.NamespaceIdentifier;
import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder;
import com.ctrip.framework.apollo.tracer.Tracer; 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.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
...@@ -29,22 +37,26 @@ import java.util.Map; ...@@ -29,22 +37,26 @@ import java.util.Map;
@Service @Service
public class ItemService { public class ItemService {
private Gson gson = new Gson();
private final UserInfoHolder userInfoHolder; private final UserInfoHolder userInfoHolder;
private final AdminServiceAPI.NamespaceAPI namespaceAPI; private final AdminServiceAPI.NamespaceAPI namespaceAPI;
private final AdminServiceAPI.ItemAPI itemAPI; private final AdminServiceAPI.ItemAPI itemAPI;
private final AdminServiceAPI.ReleaseAPI releaseAPI;
private final ConfigTextResolver fileTextResolver; private final ConfigTextResolver fileTextResolver;
private final ConfigTextResolver propertyResolver; private final ConfigTextResolver propertyResolver;
public ItemService( public ItemService(
final UserInfoHolder userInfoHolder, final UserInfoHolder userInfoHolder,
final AdminServiceAPI.NamespaceAPI namespaceAPI, final NamespaceAPI namespaceAPI,
final AdminServiceAPI.ItemAPI itemAPI, final ItemAPI itemAPI,
final ReleaseAPI releaseAPI,
final @Qualifier("fileTextResolver") ConfigTextResolver fileTextResolver, final @Qualifier("fileTextResolver") ConfigTextResolver fileTextResolver,
final @Qualifier("propertyResolver") ConfigTextResolver propertyResolver) { final @Qualifier("propertyResolver") ConfigTextResolver propertyResolver) {
this.userInfoHolder = userInfoHolder; this.userInfoHolder = userInfoHolder;
this.namespaceAPI = namespaceAPI; this.namespaceAPI = namespaceAPI;
this.itemAPI = itemAPI; this.itemAPI = itemAPI;
this.releaseAPI = releaseAPI;
this.fileTextResolver = fileTextResolver; this.fileTextResolver = fileTextResolver;
this.propertyResolver = propertyResolver; this.propertyResolver = propertyResolver;
} }
...@@ -144,6 +156,55 @@ public class ItemService { ...@@ -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) { public List<ItemDiffs> compare(List<NamespaceIdentifier> comparedNamespaces, List<ItemDTO> sourceItems) {
List<ItemDiffs> result = new LinkedList<>(); List<ItemDiffs> result = new LinkedList<>();
...@@ -231,6 +292,13 @@ public class ItemService { ...@@ -231,6 +292,13 @@ public class ItemService {
return createdItem; 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) { private boolean isModified(String sourceValue, String targetValue, String sourceComment, String targetComment) {
if (!sourceValue.equals(targetValue)) { if (!sourceValue.equals(targetValue)) {
......
...@@ -208,7 +208,8 @@ ...@@ -208,7 +208,8 @@
<apollonspanel ng-repeat="namespace in namespaces" namespace="namespace" app-id="pageContext.appId" <apollonspanel ng-repeat="namespace in namespaces" namespace="namespace" app-id="pageContext.appId"
env="pageContext.env" lock-check="lockCheck" cluster="pageContext.clusterName" user="currentUser" env="pageContext.env" lock-check="lockCheck" cluster="pageContext.clusterName" user="currentUser"
pre-release-ns="prepareReleaseNamespace" create-item="createItem" edit-item="editItem" 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" show-no-modify-permission-dialog="showNoModifyPermissionDialog" show-body="namespaces.length < 3"
lazy-load="namespaces.length > 10" pre-create-branch="preCreateBranch" lazy-load="namespaces.length > 10" pre-create-branch="preCreateBranch"
pre-delete-branch="preDeleteBranch"> pre-delete-branch="preDeleteBranch">
...@@ -318,6 +319,12 @@ ...@@ -318,6 +319,12 @@
apollo-detail="syntaxCheckContext.syntaxCheckMessage" apollo-extra-class="'pre'"> apollo-detail="syntaxCheckContext.syntaxCheckMessage" apollo-extra-class="'pre'">
</apolloconfirmdialog> </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 fade" id="createBranchTips" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
...@@ -338,6 +345,7 @@ ...@@ -338,6 +345,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -185,6 +185,8 @@ ...@@ -185,6 +185,8 @@
"Component.Namespace.Master.Items.SummitChanged": "Submit", "Component.Namespace.Master.Items.SummitChanged": "Submit",
"Component.Namespace.Master.Items.SortByKey": "Filter the configurations by key", "Component.Namespace.Master.Items.SortByKey": "Filter the configurations by key",
"Component.Namespace.Master.Items.FilterItem": "Filter", "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.SyncItemTips": "Synchronize configurations among environments",
"Component.Namespace.Master.Items.SyncItem": "Synchronize", "Component.Namespace.Master.Items.SyncItem": "Synchronize",
"Component.Namespace.Master.Items.DiffItemTips": "Compare the configurations among environments", "Component.Namespace.Master.Items.DiffItemTips": "Compare the configurations among environments",
...@@ -345,6 +347,8 @@ ...@@ -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.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.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.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.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.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", "Config.PublishNoPermission.DialogTitle": "Release",
...@@ -746,5 +750,7 @@ ...@@ -746,5 +750,7 @@
"ReleaseModal.AllPublishFailed": "Failed to Full Release", "ReleaseModal.AllPublishFailed": "Failed to Full Release",
"Rollback.NoRollbackList": "No released history to rollback", "Rollback.NoRollbackList": "No released history to rollback",
"Rollback.RollbackSuccessfully": "Rollback Successfully", "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 @@ ...@@ -187,6 +187,8 @@
"Component.Namespace.Master.Items.FilterItem": "过滤配置", "Component.Namespace.Master.Items.FilterItem": "过滤配置",
"Component.Namespace.Master.Items.SyncItemTips": "同步各环境间配置", "Component.Namespace.Master.Items.SyncItemTips": "同步各环境间配置",
"Component.Namespace.Master.Items.SyncItem": "同步配置", "Component.Namespace.Master.Items.SyncItem": "同步配置",
"Component.Namespace.Master.Items.RevokeItemTips": "撤销配置的修改",
"Component.Namespace.Master.Items.RevokeItem": "撤销配置",
"Component.Namespace.Master.Items.DiffItemTips": "比较各环境间配置", "Component.Namespace.Master.Items.DiffItemTips": "比较各环境间配置",
"Component.Namespace.Master.Items.DiffItem": "比较配置", "Component.Namespace.Master.Items.DiffItem": "比较配置",
"Component.Namespace.Master.Items.AddItem": "新增配置", "Component.Namespace.Master.Items.AddItem": "新增配置",
...@@ -345,6 +347,8 @@ ...@@ -345,6 +347,8 @@
"Config.ClusterIsDefaultTipContent": "所有不属于 '{{name}}' 集群的实例会使用default集群(当前页面)的配置,属于 '{{name}}' 的实例会使用对应集群的配置!", "Config.ClusterIsDefaultTipContent": "所有不属于 '{{name}}' 集群的实例会使用default集群(当前页面)的配置,属于 '{{name}}' 的实例会使用对应集群的配置!",
"Config.ClusterIsCustomTipContent": "属于 '{{name}}' 集群的实例只会使用 '{{name}}' 集群(当前页面)的配置,只有当对应namespace在当前集群没有发布过配置时,才会使用default集群的配置。", "Config.ClusterIsCustomTipContent": "属于 '{{name}}' 集群的实例只会使用 '{{name}}' 集群(当前页面)的配置,只有当对应namespace在当前集群没有发布过配置时,才会使用default集群的配置。",
"Config.HasNotPublishNamespace": "以下环境/集群有未发布的配置,客户端获取不到未发布的配置,请及时发布。", "Config.HasNotPublishNamespace": "以下环境/集群有未发布的配置,客户端获取不到未发布的配置,请及时发布。",
"Config.RevokeItem.DialogTitle": "撤销配置",
"Config.RevokeItem.DialogContent": "当前命名空间下已修改但尚未发布的配置将被撤销,确定要撤销么?",
"Config.DeleteItem.DialogTitle": "删除配置", "Config.DeleteItem.DialogTitle": "删除配置",
"Config.DeleteItem.DialogContent": "您正在删除 Key 为 <b> '{{config.key}}' </b> Value 为 <b> '{{config.value}}' </b> 的配置.<br>确定要删除配置吗?", "Config.DeleteItem.DialogContent": "您正在删除 Key 为 <b> '{{config.key}}' </b> Value 为 <b> '{{config.value}}' </b> 的配置.<br>确定要删除配置吗?",
"Config.PublishNoPermission.DialogTitle": "发布", "Config.PublishNoPermission.DialogTitle": "发布",
...@@ -746,5 +750,7 @@ ...@@ -746,5 +750,7 @@
"ReleaseModal.AllPublishFailed": "全量发布失败", "ReleaseModal.AllPublishFailed": "全量发布失败",
"Rollback.NoRollbackList": "没有可以回滚的发布历史", "Rollback.NoRollbackList": "没有可以回滚的发布历史",
"Rollback.RollbackSuccessfully": "回滚成功", "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 ...@@ -11,6 +11,8 @@ function controller($rootScope, $scope, $translate, toastr, AppUtil, EventManage
$scope.deleteItem = deleteItem; $scope.deleteItem = deleteItem;
$scope.editItem = editItem; $scope.editItem = editItem;
$scope.createItem = createItem; $scope.createItem = createItem;
$scope.preRevokeItem = preRevokeItem;
$scope.revokeItem = revokeItem;
$scope.closeTip = closeTip; $scope.closeTip = closeTip;
$scope.showText = showText; $scope.showText = showText;
$scope.createBranch = createBranch; $scope.createBranch = createBranch;
...@@ -178,6 +180,33 @@ function controller($rootScope, $scope, $translate, toastr, AppUtil, EventManage ...@@ -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) { function editItem(namespace, toEditItem) {
if (!lockCheck(namespace)) { if (!lockCheck(namespace)) {
......
...@@ -17,6 +17,7 @@ function directive($window, $translate, toastr, AppUtil, EventManager, Permissio ...@@ -17,6 +17,7 @@ function directive($window, $translate, toastr, AppUtil, EventManager, Permissio
createItem: '=', createItem: '=',
editItem: '=', editItem: '=',
preDeleteItem: '=', preDeleteItem: '=',
preRevokeItem: '=',
showText: '=', showText: '=',
showNoModifyPermissionDialog: '=', showNoModifyPermissionDialog: '=',
preCreateBranch: '=', preCreateBranch: '=',
......
...@@ -49,7 +49,11 @@ appService.service("ConfigService", ['$resource', '$q', 'AppUtil', function ($re ...@@ -49,7 +49,11 @@ appService.service("ConfigService", ['$resource', '$q', 'AppUtil', function ($re
syntax_check_text: { syntax_check_text: {
method: 'POST', method: 'POST',
url: AppUtil.prefixPath() + '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/syntax-check' 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 { return {
...@@ -214,6 +218,22 @@ appService.service("ConfigService", ['$resource', '$q', 'AppUtil', function ($re ...@@ -214,6 +218,22 @@ appService.service("ConfigService", ['$resource', '$q', 'AppUtil', function ($re
d.reject(result); d.reject(result);
}); });
return d.promise; 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 @@ ...@@ -183,6 +183,14 @@
{{'Component.Namespace.Master.Items.SyncItem' | translate }} {{'Component.Namespace.Master.Items.SyncItem' | translate }}
</button> </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" <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 }}" data-placement="bottom" title="{{'Component.Namespace.Master.Items.DiffItemTips' | translate }}"
ng-click="goToDiffPage(namespace)" ng-show="namespace.viewType == 'table' && namespace.displayControl.currentOperateBranch == 'master' 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