Commit 7b596e07 by lepdou

portal can edit config

parent 719d67d0
......@@ -3,6 +3,7 @@ package com.ctrip.apollo.adminservice.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
......@@ -18,8 +19,9 @@ public class ItemSetController {
private ItemSetService itemSetService;
@RequestMapping(path = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/itemset", method = RequestMethod.POST)
public ResponseEntity<Void> create(@RequestBody ItemChangeSets changeSet) {
itemSetService.updateSet(changeSet);
public ResponseEntity<Void> create(@PathVariable String appId, @PathVariable String clusterName,
@PathVariable String namespaceName, @RequestBody ItemChangeSets changeSet) {
itemSetService.updateSet(appId, clusterName, namespaceName, changeSet);
return ResponseEntity.status(HttpStatus.OK).build();
}
}
......@@ -47,12 +47,15 @@ public class ReleaseController {
@RequestMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases/latest")
public ReleaseDTO getLatest(@PathVariable("appId") String appId,
@PathVariable("clusterName") String clusterName,
@PathVariable("namespaceName") String namespaceName) {
@PathVariable("clusterName") String clusterName,
@PathVariable("namespaceName") String namespaceName) {
Release release = configService.findRelease(appId, clusterName, namespaceName);
if (release == null) throw new NotFoundException(
String.format("latest release not found for %s %s %s", appId, clusterName, namespaceName));
return BeanUtils.transfrom(ReleaseDTO.class, release);
if (release == null) {
throw new NotFoundException(
String.format("latest release not found for %s %s %s", appId, clusterName, namespaceName));
} else {
return BeanUtils.transfrom(ReleaseDTO.class, release);
}
}
@RequestMapping(path = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases", method = RequestMethod.POST)
......
......@@ -97,4 +97,5 @@ public abstract class BaseEntity {
private void preRemove() {
this.dataChangeLastModifiedTime = new Date();
}
}
......@@ -23,6 +23,9 @@ public class Item extends BaseEntity {
@Column
private String comment;
@Column
private int lineNum;
public String getComment() {
return comment;
}
......@@ -55,4 +58,11 @@ public class Item extends BaseEntity {
this.value = value;
}
public int getLineNum() {
return lineNum;
}
public void setLineNum(int lineNum) {
this.lineNum = lineNum;
}
}
......@@ -10,6 +10,6 @@ public interface ItemRepository extends PagingAndSortingRepository<Item, Long> {
List<Item> findByNamespaceIdIsIn(List<Long> namespaceIds);
List<Item> findByNamespaceId(Long namespaceId);
List<Item> findByNamespaceIdOrderByLineNumAsc(Long namespaceId);
}
......@@ -17,5 +17,5 @@ public interface ReleaseRepository extends PagingAndSortingRepository<Release, L
@Param("namespaceName") String namespaceName);
List<Release> findByAppIdAndClusterNameAndNamespaceName(String appId, String clusterName,
String namespaceName);
String namespaceName);
}
......@@ -4,32 +4,50 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ctrip.apollo.biz.entity.Item;
import com.ctrip.apollo.biz.entity.Namespace;
import com.ctrip.apollo.biz.repository.ItemRepository;
import com.ctrip.apollo.biz.repository.NamespaceRepository;
import com.ctrip.apollo.biz.utils.BeanUtils;
import com.ctrip.apollo.core.dto.ItemChangeSets;
import com.ctrip.apollo.core.dto.ItemDTO;
import java.util.Date;
@Service
public class ItemSetService {
@Autowired
private ItemRepository itemRepository;
@Autowired
private NamespaceRepository namespaceRepository;
public void updateSet(ItemChangeSets changeSet) {
public void updateSet(String appId, String clusterName, String namespaceName, ItemChangeSets changeSet) {
Namespace namespace = namespaceRepository.findByAppIdAndClusterNameAndNamespaceName(appId, clusterName, namespaceName);
String modifyBy = changeSet.getModifyBy();
for (ItemDTO item : changeSet.getCreateItems()) {
Item entity = BeanUtils.transfrom(Item.class, item);
entity.setNamespaceId(namespace.getId());
entity.setDataChangeCreatedBy(modifyBy);
entity.setDataChangeCreatedTime(new Date());
entity.setDataChangeLastModifiedBy(modifyBy);
itemRepository.save(entity);
}
for (ItemDTO item : changeSet.getUpdateItems()) {
Item entity = BeanUtils.transfrom(Item.class, item);
Item managedItem = itemRepository.findOne(entity.getId());
BeanUtils.copyEntityProperties(entity, managedItem);
if (managedItem != null){
BeanUtils.copyEntityProperties(entity, managedItem, "id", "namespaceId", "key", "dataChangeCreatedBy", "dataChangeCreatedTime");
managedItem.setDataChangeLastModifiedBy(modifyBy);
}
itemRepository.save(managedItem);
}
for (ItemDTO item : changeSet.getDeletedItems()) {
Item entity = BeanUtils.transfrom(Item.class, item);
entity.setDataChangeLastModifiedBy(modifyBy);
itemRepository.delete(entity.getId());
}
}
......
......@@ -46,7 +46,7 @@ public class ReleaseService {
throw new NotFoundException(String.format("Could not find namespace for %s %s %s", appId,
clusterName, namespaceName));
}
List<Item> items = itemRepository.findByNamespaceId(namespace.getId());
List<Item> items = itemRepository.findByNamespaceIdOrderByLineNumAsc(namespace.getId());
Map<String, String> configurations = new HashMap<String, String>();
for (Item item : items) {
configurations.put(item.getKey(), item.getValue());
......
......@@ -65,7 +65,7 @@ public class ViewService {
}
public List<Item> findItems(Long namespaceId) {
List<Item> items = itemRepository.findByNamespaceId(namespaceId);
List<Item> items = itemRepository.findByNamespaceIdOrderByLineNumAsc(namespaceId);
if (items == null) {
return Collections.EMPTY_LIST;
}
......
......@@ -214,7 +214,7 @@ public class BeanUtils {
* @param source
* @param target
*/
public static void copyEntityProperties(Object source, Object target) {
org.springframework.beans.BeanUtils.copyProperties(source, target, "id");
public static void copyEntityProperties(Object source, Object target, String... ignoreProperties) {
org.springframework.beans.BeanUtils.copyProperties(source, target, ignoreProperties);
}
}
......@@ -9,28 +9,19 @@ import java.util.List;
public class ItemChangeSets {
private String modifyBy;
private List<ItemDTO> createItems;
private List<ItemDTO> updateItems;
private List<ItemDTO> deletedItems;
private List<ItemDTO> createItems = new LinkedList<>();
private List<ItemDTO> updateItems = new LinkedList<>();
private List<ItemDTO> deletedItems = new LinkedList<>();
public void addCreatedItem(ItemDTO item) {
if (createItems == null) {
createItems = new LinkedList<>();
}
createItems.add(item);
}
public void addupdateItem(ItemDTO item) {
if (updateItems == null) {
updateItems = new LinkedList<>();
}
public void addUpdateItem(ItemDTO item) {
updateItems.add(item);
}
public void addDeletedItem(ItemDTO item) {
if (deletedItems == null) {
deletedItems = new LinkedList<>();
}
deletedItems.add(item);
}
......
......@@ -2,7 +2,7 @@ package com.ctrip.apollo.core.dto;
import java.util.Date;
public class ItemDTO {
public class ItemDTO{
private long id;
......@@ -14,6 +14,8 @@ public class ItemDTO {
private String comment;
private int lineNum;
private String dataChangeLastModifiedBy;
private Date dataChangeLastModifiedTime;
......@@ -67,6 +69,14 @@ public class ItemDTO {
this.value = value;
}
public int getLineNum() {
return lineNum;
}
public void setLineNum(int lineNum) {
this.lineNum = lineNum;
}
public String getDataChangeLastModifiedBy() {
return dataChangeLastModifiedBy;
}
......@@ -91,8 +101,10 @@ public class ItemDTO {
", key='" + key + '\'' +
", value='" + value + '\'' +
", comment='" + comment + '\'' +
", lineNum=" + lineNum +
", dataChangeLastModifiedBy='" + dataChangeLastModifiedBy + '\'' +
", dataChangeLastModifiedTime=" + dataChangeLastModifiedTime +
'}';
}
}
package com.ctrip.apollo.core.entity;
/**
* declare biz code and simple msg. maybe http response code is 200.
*/
public class SimpleRestfulResponse {
private int code;
private String msg;
public SimpleRestfulResponse(int code, String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
......@@ -4,12 +4,17 @@ package com.ctrip.apollo.portal.api;
import com.ctrip.apollo.Apollo;
import com.ctrip.apollo.core.dto.AppDTO;
import com.ctrip.apollo.core.dto.ClusterDTO;
import com.ctrip.apollo.core.dto.ItemChangeSets;
import com.ctrip.apollo.core.dto.ItemDTO;
import com.ctrip.apollo.core.dto.NamespaceDTO;
import com.ctrip.apollo.core.dto.ReleaseDTO;
import com.ctrip.apollo.core.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import java.util.Arrays;
import java.util.List;
......@@ -17,6 +22,7 @@ import java.util.List;
@Service
public class AdminServiceAPI {
private static final Logger logger = LoggerFactory.getLogger(AdminServiceAPI.class);
@Service
public static class AppAPI extends API {
......@@ -33,7 +39,7 @@ public class AdminServiceAPI {
public static class NamespaceAPI extends API {
public List<NamespaceDTO> findGroupsByAppAndCluster(String appId, Apollo.Env env,
String clusterName) {
String clusterName) {
if (StringUtils.isContainEmpty(appId, clusterName)) {
return null;
}
......@@ -48,52 +54,70 @@ public class AdminServiceAPI {
if (StringUtils.isContainEmpty(appId, clusterName, namespaceName)) {
return null;
}
return restTemplate.getForObject(getAdminServiceHost(env) +
String.format("apps/%s/clusters/%s/namespaces/%s", appId, clusterName,
namespaceName), NamespaceDTO.class);
}
return restTemplate.getForObject(getAdminServiceHost(env) +
String.format("apps/%s/clusters/%s/namespaces/%s", appId, clusterName,
namespaceName), NamespaceDTO.class);
}
}
@Service
public static class ItemAPI extends API {
public List<ItemDTO> findItems(String appId, Apollo.Env env, String clusterName, String namespace) {
if (StringUtils.isContainEmpty(appId, clusterName, namespace)) {
return null;
}
@Service
public static class ItemAPI extends API {
return Arrays.asList(restTemplate.getForObject(getAdminServiceHost(env) + String
.format("apps/%s/clusters/%s/namespaces/%s/items", appId,
clusterName, namespace),
ItemDTO[].class));
public List<ItemDTO> findItems(String appId, Apollo.Env env, String clusterName, String namespace) {
if (StringUtils.isContainEmpty(appId, clusterName, namespace)) {
return null;
}
return Arrays.asList(restTemplate.getForObject(getAdminServiceHost(env) + String
.format("apps/%s/clusters/%s/namespaces/%s/items", appId,
clusterName, namespace),
ItemDTO[].class));
}
@Service
public static class ClusterAPI extends API {
public void updateItems(String appId, Apollo.Env env, String clusterName, String namespace,
ItemChangeSets changeSets) {
if (StringUtils.isContainEmpty(appId, clusterName, namespace)){
return;
}
restTemplate.postForEntity(getAdminServiceHost(env) + String.format("apps/%s/clusters/%s/namespaces/%s/itemset",
appId,clusterName, namespace), changeSets, Void.class);
}
}
public List<ClusterDTO> findClustersByApp(String appId, Apollo.Env env) {
if (StringUtils.isContainEmpty(appId)) {
return null;
}
@Service
public static class ClusterAPI extends API {
return Arrays.asList(restTemplate.getForObject(getAdminServiceHost(env) + String.format("apps/%s/clusters", appId),
ClusterDTO[].class));
public List<ClusterDTO> findClustersByApp(String appId, Apollo.Env env) {
if (StringUtils.isContainEmpty(appId)) {
return null;
}
return Arrays
.asList(restTemplate.getForObject(getAdminServiceHost(env) + String.format("apps/%s/clusters", appId),
ClusterDTO[].class));
}
}
@Service
public static class ReleaseAPI extends API {
@Service
public static class ReleaseAPI extends API {
public ReleaseDTO loadLatestRelease(String appId, Apollo.Env env, String clusterName, String namespace) {
if (StringUtils.isContainEmpty(appId, clusterName, namespace)) {
return null;
}
return restTemplate.getForObject(getAdminServiceHost(env) + String
.format("apps/%s/clusters/%s/namespaces/%s/releases/latest", appId,
clusterName, namespace), ReleaseDTO.class);
public ReleaseDTO loadLatestRelease(String appId, Apollo.Env env, String clusterName, String namespace) {
if (StringUtils.isContainEmpty(appId, clusterName, namespace)) {
return null;
}
try {
ReleaseDTO releaseDTO = restTemplate.getForObject(getAdminServiceHost(env) + String
.format("apps/%s/clusters/%s/namespaces/%s/releases/latest", appId,
clusterName, namespace), ReleaseDTO.class);
return releaseDTO;
}catch (HttpClientErrorException e){
logger.warn(" call [ReleaseAPI.loadLatestRelease] and return not fount exception.app id:{}, env:{}, clusterName:{}, namespace:{}",
appId, env, clusterName, namespace);
return null;
}
}
}
}
......@@ -26,5 +26,7 @@ public class AppController {
return appService.buildClusterNavTree(appId);
}
}
......@@ -2,15 +2,13 @@ package com.ctrip.apollo.portal.controller;
import com.ctrip.apollo.Apollo;
import com.ctrip.apollo.core.entity.SimpleRestfulResponse;
import com.ctrip.apollo.core.utils.StringUtils;
import com.ctrip.apollo.portal.entity.NamespaceVO;
import com.ctrip.apollo.portal.entity.SimpleResponse;
import com.ctrip.apollo.portal.entity.SimpleMsg;
import com.ctrip.apollo.portal.service.ConfigService;
import com.ctrip.apollo.portal.service.txtresolver.TextResolverResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
......@@ -37,19 +35,16 @@ public class ConfigController {
}
@RequestMapping(value = "/apps/{appId}/env/{env}/clusters/{clusterName}/namespaces/{namespaceName}/modify", method = RequestMethod.GET)
public ResponseEntity<SimpleRestfulResponse> modifyConfigs(@PathVariable String appId, @PathVariable String env,
@PathVariable String clusterName,
@PathVariable String namespaceName,
String configText) {
public ResponseEntity<SimpleMsg> modifyConfigs(@PathVariable String appId, @PathVariable String env,
@PathVariable String clusterName,
@PathVariable String namespaceName,
String configText) {
TextResolverResult result =
configService.resolve(appId, Apollo.Env.valueOf(env), clusterName, namespaceName, configText);
TextResolverResult.Code code = result.getCode();
if (code == TextResolverResult.Code.OK) {
return ResponseEntity.status(HttpStatus.OK).body(new SimpleRestfulResponse(code.getValue(), "success"));
if (result.isResolveSuccess()) {
return ResponseEntity.ok().body(new SimpleMsg("success"));
} else {
return ResponseEntity.status(HttpStatus.OK)
.body(new SimpleRestfulResponse(code.getValue(), code.getBaseMsg() + result.getExtensionMsg()));
return ResponseEntity.badRequest().body(new SimpleMsg(result.getMsg()));
}
}
......
......@@ -71,6 +71,7 @@ public class NamespaceVO {
public void setNewValue(String newValue) {
this.newValue = newValue;
}
}
}
package com.ctrip.apollo.portal.entity;
public class SimpleResponse {
public class SimpleMsg {
private int code;
private String msg;
public SimpleResponse(int code, String msg){
this.code = code;
public SimpleMsg(String msg){
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
......
......@@ -50,19 +50,51 @@ public class ConfigService {
List<NamespaceVO> namespaceVOs = new LinkedList<>();
for (NamespaceDTO namespace : namespaces) {
namespaceVOs.add(parseNamespace(appId, env, clusterName, namespace));
NamespaceVO namespaceVO = null;
try {
namespaceVO = parseNamespace(appId, env, clusterName, namespace);
namespaceVOs.add(namespaceVO);
} catch (Exception e) {
logger.error("parse namespace error. app id:{}, env:{}, clusterName:{}, namespace:{}", appId, env, clusterName,
namespace.getNamespaceName(), e);
return namespaceVOs;
}
}
return namespaceVOs;
}
public TextResolverResult resolve(String appId, Apollo.Env env, String clusterName, String namespaceName,
String configText) {
TextResolverResult result = resolver.resolve(configText, itemAPI.findItems(appId, env, clusterName, namespaceName));
if (result.getCode() == TextResolverResult.Code.OK) {
ItemChangeSets changeSets = result.getChangeSets();
//invoke admin service
String configText) {
TextResolverResult result = new TextResolverResult();
try {
result = resolver.resolve(configText, itemAPI.findItems(appId, env, clusterName, namespaceName));
} catch (Exception e) {
logger
.error("resolve config text error. app id:{}, env:{}, clusterName:{}, namespace:{}", appId, env, clusterName,
namespaceName, e);
result.setResolveSuccess(false);
result.setMsg("oops! server resolve config text error.");
return result;
}
if (result.isResolveSuccess()) {
try {
// TODO: 16/4/13
result.getChangeSets().setModifyBy("lepdou");
itemAPI.updateItems(appId, env, clusterName, namespaceName, result.getChangeSets());
} catch (Exception e) {
logger.error("resolve config text error. app id:{}, env:{}, clusterName:{}, namespace:{}", appId, env,
clusterName, namespaceName, e);
result.setResolveSuccess(false);
result.setMsg("oops! server update config error.");
return result;
}
} else {
logger.warn("resolve config text error by format error. app id:{}, env:{}, clusterName:{}, namespace:{},cause:{}",
appId,env, clusterName, namespaceName, result.getMsg());
}
return result;
}
......@@ -72,6 +104,7 @@ public class ConfigService {
namespaceVO.setNamespace(namespace);
List<NamespaceVO.ItemVO> itemVos = new LinkedList<>();
namespaceVO.setItems(itemVos);
String namespaceName = namespace.getNamespaceName();
......@@ -84,6 +117,7 @@ public class ConfigService {
} catch (IOException e) {
logger.error("parse release json error. appId:{},env:{},clusterName:{},namespace:{}", appId,
env, clusterName, namespaceName);
return namespaceVO;
}
}
......@@ -101,7 +135,6 @@ public class ConfigService {
itemVos.add(itemVO);
}
namespaceVO.setItemModifiedCnt(modifiedItemCnt);
namespaceVO.setItems(itemVos);
return namespaceVO;
}
......
package com.ctrip.apollo.portal.service.txtresolver;
import com.ctrip.apollo.core.dto.ItemChangeSets;
import com.ctrip.apollo.core.dto.ItemDTO;
import com.ctrip.apollo.core.utils.StringUtils;
import com.ctrip.apollo.portal.util.BeanUtils;
import com.sun.tools.javac.util.Assert;
import org.apache.commons.collections.map.HashedMap;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* normal property file resolver.
* update comment and blank item implement by create new item and delete old item.
* update normal key/value item implement by update.
*/
@Component
public class PropertyResolver implements ConfigTextResolver {
private static final String KV_SEPARATOR = "=";
private static final String ITEM_SEPARATOR = "\n";
@Override
public TextResolverResult resolve(String configText, List<ItemDTO> baseItems) {
TextResolverResult result = new TextResolverResult();
if (StringUtils.isEmpty(configText)){
result.setResolveSuccess(false);
result.setMsg("config text can not be empty");
return result;
}
Map<Integer, ItemDTO> oldLineNumMapItem = BeanUtils.mapByKey("lineNum", baseItems);
Map<String, ItemDTO> oldKeyMapItem = BeanUtils.mapByKey("key", baseItems);
//remove comment and blank item map.
oldKeyMapItem.remove("");
String[] newItems = configText.split(ITEM_SEPARATOR);
ItemChangeSets changeSets = new ItemChangeSets();
result.setChangeSets(changeSets);
Map<Integer, String> newLineNumMapItem = new HashedMap();//use for delete blank and comment item
int lineCounter = 1;
for (String newItem : newItems) {
newItem = newItem.trim();
newLineNumMapItem.put(lineCounter, newItem);
ItemDTO oldItemByLine = oldLineNumMapItem.get(lineCounter);
//comment item
if (isCommentItem(newItem)) {
handleCommentLine(oldItemByLine, newItem, lineCounter, changeSets);
//blank item
} else if (isBlankItem(newItem)) {
handleBlankLine(oldItemByLine, lineCounter, changeSets);
//normal line
} else {
if (!handleNormalLine(oldKeyMapItem, newItem, lineCounter, result)) {
return result;
}
}
lineCounter++;
}
deleteCommentAndBlankItem(oldLineNumMapItem, newLineNumMapItem, changeSets);
deleteNormalKVItem(oldKeyMapItem, changeSets);
result.setResolveSuccess(true);
return result;
}
private void handleCommentLine(ItemDTO oldItemByLine, String newItem, int lineCounter, ItemChangeSets changeSets) {
String oldComment = oldItemByLine == null ? "" : oldItemByLine.getComment();
//create comment. implement update comment by delete old comment and create new comment
if (!(isCommentItem(oldItemByLine) && newItem.equals(oldComment))) {
changeSets.addCreatedItem(buildCommentItem(0l, newItem, lineCounter));
}
}
private void handleBlankLine(ItemDTO oldItem, int lineCounter, ItemChangeSets changeSets) {
if (!isBlankItem(oldItem)) {
changeSets.addCreatedItem(buildBlankItem(0l, lineCounter));
}
}
private boolean handleNormalLine(Map<String, ItemDTO> keyMapOldItem, String newItem,
int lineCounter, TextResolverResult result) {
ItemChangeSets changeSets = result.getChangeSets();
int kvSeparator = newItem.indexOf(KV_SEPARATOR);
if (kvSeparator == -1) {
result.setResolveSuccess(false);
result.setMsg(" line:" + lineCounter + " key value must separate by '='");
return false;
}
String newKey = newItem.substring(0, kvSeparator).trim();
String newValue = newItem.substring(kvSeparator + 1, newItem.length()).trim();
ItemDTO oldItem = keyMapOldItem.get(newKey);
if (oldItem == null) {//new item
changeSets.addCreatedItem(buildNormalItem(0l, newKey, newValue, "", lineCounter));
} else if (!newValue.equals(oldItem.getValue())){//update item
changeSets.addUpdateItem(
buildNormalItem(oldItem.getId(), newKey, newValue, oldItem.getComment(),
lineCounter));
}
keyMapOldItem.remove(newKey);
return true;
}
private boolean isCommentItem(ItemDTO item) {
return item != null && "".equals(item.getKey()) && item.getComment().startsWith("#");
}
private boolean isCommentItem(String line) {
return line != null && line.startsWith("#");
}
private boolean isBlankItem(ItemDTO item) {
return item != null && "".equals(item.getKey()) && "".equals(item.getComment());
}
private boolean isBlankItem(String line) {
return "".equals(line);
}
private void deleteNormalKVItem(Map<String, ItemDTO> baseKeyMapItem, ItemChangeSets changeSets) {
//surplus item is to be deleted
for (Map.Entry<String, ItemDTO> entry : baseKeyMapItem.entrySet()) {
changeSets.addDeletedItem(entry.getValue());
}
}
private void deleteCommentAndBlankItem(Map<Integer, ItemDTO> oldLineNumMapItem,
Map<Integer, String> newLineNumMapItem,
ItemChangeSets changeSets) {
for (Map.Entry<Integer, ItemDTO> entry : oldLineNumMapItem.entrySet()) {
int lineNum = entry.getKey();
ItemDTO oldItem = entry.getValue();
String newItem = newLineNumMapItem.get(lineNum);
//1. old is blank by now is not
//2.old is comment by now is not exist or modified
if ((isBlankItem(oldItem) && !isBlankItem(newItem))
|| isCommentItem(oldItem) && (newItem == null || !newItem.equals(oldItem))) {
changeSets.addDeletedItem(oldItem);
}
}
}
private ItemDTO buildCommentItem(Long id, String comment, int lineNum) {
return buildNormalItem(id, "", "", comment, lineNum);
}
private ItemDTO buildBlankItem(Long id, int lineNum) {
return buildNormalItem(id, "", "", "", lineNum);
}
private ItemDTO buildNormalItem(Long id, String key, String value, String comment, int lineNum) {
ItemDTO item = new ItemDTO();
item.setId(id);
item.setKey(key);
item.setValue(value);
item.setComment(comment);
item.setLineNum(lineNum);
return item;
}
}
package com.ctrip.apollo.portal.service.txtresolver;
import com.ctrip.apollo.core.dto.ItemChangeSets;
import com.ctrip.apollo.core.dto.ItemDTO;
import com.ctrip.apollo.core.utils.StringUtils;
import com.ctrip.apollo.portal.util.BeanUtils;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* config item format is K:V##C
*
* @Autor lepdou
*/
@Component
public class SimpleKVCResolver implements ConfigTextResolver {
private static final String KV_SEPARATOR = ":";
private static final String VC_SEPARATOR = "##";
private static final String ITEM_SEPARATOR = "\n";
@Override
public TextResolverResult resolve(String configText, List<ItemDTO> baseItems) {
TextResolverResult result = new TextResolverResult();
if (StringUtils.isEmpty(configText)) {
result.setCode(TextResolverResult.Code.SIMPLE_KVC_TEXT_EMPTY);
return result;
}
Map<String, ItemDTO> baseKeyMapItem = BeanUtils.mapByKey("key", baseItems);
String[] items = configText.split(ITEM_SEPARATOR);
ItemChangeSets changeSets = new ItemChangeSets();
int lineCounter = 1;
int kvSeparator, vcSeparator;
String key, value, comment;
for (String item : items) {
kvSeparator = item.indexOf(KV_SEPARATOR);
vcSeparator = item.indexOf(VC_SEPARATOR);
if (kvSeparator == -1 || vcSeparator == -1) {
result.setCode(TextResolverResult.Code.SIMPLTE_KVC_INVALID_FORMAT);
result.setExtensionMsg(" line:" + lineCounter);
return result;
}
key = item.substring(0, kvSeparator).trim();
value = item.substring(kvSeparator + 1, vcSeparator).trim();
comment = item.substring(vcSeparator + 2, item.length()).trim();
ItemDTO baseItem = baseKeyMapItem.get(key);
if (baseItem == null) {//new item
changeSets.addCreatedItem(buildItem(key, value, comment));
} else if (!value.equals(baseItem.getValue()) || !comment.equals(baseItem.getComment())) {//update item
changeSets.addupdateItem(buildItem(key, value, comment));
}
//deleted items:items in baseItems but not in configText
baseKeyMapItem.remove(key);
lineCounter ++;
}
//deleted items
for (Map.Entry<String, ItemDTO> entry : baseKeyMapItem.entrySet()) {
changeSets.addDeletedItem(entry.getValue());
}
result.setCode(TextResolverResult.Code.OK);
result.setChangeSets(changeSets);
return result;
}
private ItemDTO buildItem(String key, String value, String comment) {
ItemDTO item = new ItemDTO();
item.setKey(key);
item.setValue(value);
item.setComment(comment);
return item;
}
}
......@@ -4,28 +4,27 @@ import com.ctrip.apollo.core.dto.ItemChangeSets;
public class TextResolverResult {
private Code code;
private boolean isResolveSuccess;
/**
* extension msg. for example line number.
* error msg
*/
private String extensionMsg = "";
private String msg = "";
private ItemChangeSets changeSets;
public Code getCode() {
return code;
public boolean isResolveSuccess() {
return isResolveSuccess;
}
public void setCode(Code code) {
this.code = code;
public void setResolveSuccess(boolean resolveSuccess) {
isResolveSuccess = resolveSuccess;
}
public String getExtensionMsg() {
return extensionMsg;
public String getMsg() {
return msg;
}
public void setExtensionMsg(String extensionMsg) {
this.extensionMsg = extensionMsg;
public void setMsg(String msg) {
this.msg = msg;
}
public ItemChangeSets getChangeSets() {
......@@ -36,32 +35,4 @@ public class TextResolverResult {
this.changeSets = changeSets;
}
public enum Code {
OK(200, "success"), SIMPLTE_KVC_INVALID_FORMAT(40001, "item pattern must key:value##comment.pelease check!"),
SIMPLE_KVC_TEXT_EMPTY(40002, "config text empty");
private int value;
private String baseMsg;
Code(int value, String msg) {
this.value = value;
this.baseMsg = msg;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public String getBaseMsg() {
return baseMsg;
}
public void setBaseMsg(String baseMsg) {
this.baseMsg = baseMsg;
}
}
}
......@@ -14,7 +14,7 @@ create_app_module.controller('CreateAppController', ['$scope', '$window', 'toast
AppService.add($scope.app).then(function (result) {
toastr.success('添加成功!');
setInterval(function () {
$window.location.href = '/views/app/index.html?#appid=' + result.appId;
$window.location.href = '/views/app.html?#appid=' + result.appId;
}, 1000);
}, function (result) {
toastr.error('添加失败!');
......
......@@ -2,11 +2,18 @@ application_module.controller("AppConfigController",
['$scope', '$location', 'toastr', 'AppService', 'ConfigService',
function ($scope, $location, toastr, AppService, ConfigService) {
$scope.appId = $location.$$url.split("=")[1];
var appId = $location.$$url.split("=")[1];
var pageContext = {
appId: appId,
env: 'LOCAL',
clusterName: 'default'
};
$scope.pageEnv = pageContext;
/////////////
AppService.load_nav_tree($scope.appId).then(function (result) {
AppService.load_nav_tree($scope.pageEnv.appId).then(function (result) {
var navTree = [];
var nodes = result.nodes;
nodes.forEach(function (item) {
......@@ -37,9 +44,8 @@ application_module.controller("AppConfigController",
///////////
$scope.env = 'LOCAL';
$scope.clusterName = 'default';
ConfigService.load_all_namespaces($scope.appId, $scope.env, $scope.clusterName).then(
ConfigService.load_all_namespaces($scope.pageEnv.appId, $scope.pageEnv.env,
$scope.pageEnv.clusterName).then(
function (result) {
$scope.namespaces = result;
......@@ -48,30 +54,49 @@ application_module.controller("AppConfigController",
$scope.namespaces.forEach(function (item) {
item.isModify = false;
item.viewType = 'table';
item.isTextEditing = false;
})
}
}, function (result) {
toastr.error("加载配置信息出错:" + result);
toastr.error("加载配置信息出错");
});
$scope.draft = {};
//保存草稿
$scope.saveDraft = function (namespace) {
$scope.draft = namespace;
};
//更新配置
$scope.modifyItems = function (namespace) {
ConfigService.modify_items($scope.appId, $scope.env, $scope.clusterName,
namespace.namespace.namespaceName, namespace.text).then(
$scope.commitChange = function () {
ConfigService.modify_items($scope.pageEnv.appId, $scope.pageEnv.env, $scope.pageEnv.clusterName,
$scope.draft.namespace.namespaceName, $scope.draft.text).then(
function (result) {
if (result.code == 200){
toastr.success("更新成功");
}else {
toastr.error("更新失败. code:" + result.code + " msg:" + result.msg);
}
},function (result) {
toastr.success("更新成功");
$scope.draft.backupText = '';//清空备份文本
$scope.toggleTextEditStatus($scope.draft);
}, function (result) {
toastr.error(result.data.msg, "更新失败");
}
);
};
/////////
//文本编辑框状态切换
$scope.toggleTextEditStatus = function (namespace) {
namespace.isTextEditing = !namespace.isTextEditing;
if (namespace.isTextEditing){//切换为编辑状态,保存一下原来值
$scope.draft.backupText = namespace.text;
}else {
if ($scope.draft.backupText){//取消编辑,则复原
namespace.text = $scope.draft.backupText;
}
}
};
$scope.queryOldValue = function (key, oldValue) {
$scope.queryKey = key;
if (oldValue == '') {
......@@ -101,8 +126,13 @@ application_module.controller("AppConfigController",
// if (item.modified) {
// result += "**";
// }
result +=
item.item.key + ":" + item.item.value + " ##" + item.item.comment + "\n";
if (item.item.key) {
result +=
item.item.key + " = " + item.item.value + "\n";
} else {
result += item.item.comment + "\n";
}
});
return result;
......
application_module.controller("AppInfoController", ["$scope", '$rootScope', '$state', '$location', 'toastr', 'AppService',
function ($scope, $rootScope, $state, $location, toastr, AppService) {
$rootScope.breadcrumb.nav = '应用信息';
$rootScope.breadcrumb.env = '';
AppService.load($scope.appId).then(function (result) {
$scope.app = result;
}, function (result) {
toastr.error("加载出错");
});
}]);
//page context ctl
application_module.controller("AppPageController", ['$rootScope', '$location',
function ($rootScope, $location) {
$rootScope.appId = $location.$$url.split("=")[1];
if (!$rootScope.appId) {
$rootScope.appId = 6666;
}
$rootScope.breadcrumb = {
project: '6666-apollo',
nav: '配置',
env: 'uat'
}
}]);
appService.service('EnvService', ['$resource', '$q', function ($resource, $q) {
var env_resource = $resource('/envs', {}, {
all: {
method: 'GET',
isArray: true
}
});
return {
getAllEnvs: function getAllEnvs() {
var d = $q.defer();
env_resource.all({}, function (result) {
d.resolve(result);
}, function (result) {
d.reject(result);
});
return d.promise;
}
}
}]);
appService.service("VersionService", ['$resource', '$q', function ($resource, $q) {
var config_source = $resource("/version/:appId/:env", {}, {
load_config: {
method: 'GET',
isArray: true
}
});
return {
load: function (appId, env) {
var d = $q.defer();
config_source.load_config({
appId: appId,
env: env
}, function (result) {
d.resolve(result);
}, function (result) {
d.reject(result);
});
return d.promise;
}
}
}]);
......@@ -4,15 +4,15 @@
<head>
<meta charset="UTF-8">
<title>apollo</title>
<link rel="stylesheet" type="text/css" href="../../vendor/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="../../vendor/angular/angular-toastr-1.4.1.min.css">
<link rel="stylesheet" type="text/css" media='all' href="../../vendor/angular/loading-bar.min.css">
<link rel="stylesheet" type="text/css" href="../../styles/common-style.css">
<link rel="stylesheet" type="text/css" href="../vendor/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="../vendor/angular/angular-toastr-1.4.1.min.css">
<link rel="stylesheet" type="text/css" media='all' href="../vendor/angular/loading-bar.min.css">
<link rel="stylesheet" type="text/css" href="../styles/common-style.css">
</head>
<body>
<div ng-include="'../common/nav.html'"></div>
<div ng-include="'common/nav.html'"></div>
<div class="container-fluid">
<div class="app" ng-controller="AppConfigController as appConfig">
......@@ -81,25 +81,33 @@
</div>
<div class="col-md-1 text-right">
&nbsp;
<a data-toggle="tooltip" data-placement="top" title="修改配置" ng-click="modifyItems(namespace)">
<a data-toggle="tooltip" data-placement="top" title="取消修改"
ng-show="namespace.isTextEditing" ng-click="toggleTextEditStatus(namespace)">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</a>
<a data-toggle="tooltip" data-placement="top" title="修改配置"
ng-show="!namespace.isTextEditing"
ng-click="toggleTextEditStatus(namespace)">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
</a>
&nbsp;
<a data-toggle="modal" data-target="#commitModal"
ng-show="namespace.isTextEditing" ng-click="saveDraft(namespace)">
<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
</a>
</div>
</div>
</header>
<div ng-show="namespace.viewType == 'textarea'">
<textarea class="form-control" rows="3" disabled>
书写格式:key:value##comment
说明:key和value之间‘:’隔开,每个key的备注在value后面,且两个‘#’号隔开。
</textarea>
<textarea class="form-control" rows="20" ng-model="namespace.text">
<textarea class="form-control" rows="20" ng-model="namespace.text"
ng-disabled="!namespace.isTextEditing">
{{namespace.text}}
</textarea>
</div>
<table class="table table-bordered text-center table-hover" ng-show="namespace.viewType == 'table'">
<table class="table table-bordered text-center table-hover"
ng-show="namespace.viewType == 'table'">
<thead>
<tr>
<th>
......@@ -121,14 +129,14 @@
</thead>
<tbody ng-repeat="config in namespace.items">
<tr ng-class="{warning:config.modified}">
<tr ng-class="{warning:config.modified}" ng-if="config.item.key">
<td>
{{config.item.key}}
</td>
<td>
<button data-placement="top" title="查看旧值"
class="glyphicon glyphicon-eye-open"
aria-hidden="true" data-toggle="modal" data-target="#oldValue"
aria-hidden="true" data-toggle="modal" data-target="#oldValueModal"
ng-show="config.modified"
ng-click="queryOldValue(config.item.key, config.oldValue)"></button>
{{config.item.value}}
......@@ -153,7 +161,7 @@
<div class="row">
<div class="col-md-11 col-md-offset-1 list" style="">
<div class="media">
<img src="../../img/history.png"/>
<img src="../img/history.png"/>
<div class="row">
<div class="col-md-10"><h5 class="media-heading">2016-02-23
......@@ -171,7 +179,7 @@
<hr>
</div>
<div class="media">
<img src="../../img/history.png"/>
<img src="../img/history.png"/>
<div class="row">
<div class="col-md-10"><h5 class="media-heading">2016-02-23
......@@ -201,7 +209,7 @@
</div>
<!-- Modal -->
<div class="modal fade " id="oldValue" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal fade " id="oldValueModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content">
<div class="modal-header panel-primary">
......@@ -218,44 +226,62 @@
</div>
</div>
</div>
<!-- commint modify config -->
<div class="modal fade" id="commitModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel2">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header panel-primary">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel2">Commit changes</h4>
</div>
<div class="modal-body">
<textarea rows="4" style="width:570px;" placeholder="input change log...."></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal" ng-click="commitChange()">提交</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div ng-include="'../common/footer.html'"></div>
<div ng-include="'common/footer.html'"></div>
<!-- jquery.js -->
<script src="../../vendor/jquery.js" type="text/javascript"></script>
<script src="../vendor/jquery.js" type="text/javascript"></script>
<!--lodash.js-->
<script src="../../vendor/lodash.min.js" type="text/javascript"></script>
<script src="../vendor/lodash.min.js" type="text/javascript"></script>
<!--angular-->
<script src="../../vendor/angular/angular.min.js"></script>
<script src="../../vendor/angular/angular-ui-router.min.js"></script>
<script src="../../vendor/angular/angular-resource.min.js"></script>
<script src="../../vendor/angular/angular-toastr-1.4.1.tpls.min.js"></script>
<script src="../../vendor/angular/loading-bar.min.js"></script>
<script src="../vendor/angular/angular.min.js"></script>
<script src="../vendor/angular/angular-ui-router.min.js"></script>
<script src="../vendor/angular/angular-resource.min.js"></script>
<script src="../vendor/angular/angular-toastr-1.4.1.tpls.min.js"></script>
<script src="../vendor/angular/loading-bar.min.js"></script>
<!-- bootstrap.js -->
<script src="../../vendor/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
<script src="../../vendor/bootstrap/js/bootstrap-treeview.min.js" type="text/javascript"></script>
<script src="../vendor/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
<script src="../vendor/bootstrap/js/bootstrap-treeview.min.js" type="text/javascript"></script>
<!--biz script-->
<script type="application/javascript" src="../../scripts/app.js"></script>
<script type="application/javascript" src="../scripts/app.js"></script>
<!--directive-->
<!--service-->
<script type="application/javascript" src="../../scripts/services/AppService.js"></script>
<script type="application/javascript" src="../../scripts/services/EnvService.js"></script>
<script type="application/javascript" src="../../scripts/services/ConfigService.js"></script>
<script type="application/javascript" src="../../scripts/services/VersionService.js"></script>
<script type="application/javascript" src="../scripts/services/AppService.js"></script>
<script type="application/javascript" src="../scripts/services/ConfigService.js"></script>
<!--controller-->
<script type="application/javascript" src="../../scripts/controller/app/AppConfigController.js"></script>
<script type="application/javascript" src="../scripts/controller/app/AppConfigController.js"></script>
<script type="application/javascript">
......
<!--配置信息-->
<div class="tab-pane active" id="config-info" style="min-height: 500px;">
<!--环境nav-->
<ul class="nav nav-pills nav-justified">
<li ng-repeat="env in envs" ng-class="{active:configLocation.env == env}">
<a ng-click="switchEnv(env)">{{env}}</a>
</li>
</ul>
<!--具体配置信息-->
<div class="row config-info-container">
<!--tag导航-->
<div class="col-md-2">
<ul class="nav nav-pills nav-stacked">
<li class="dropdown-header">未发布</li>
<li role="presentation" ng-class="{active:configLocation.versionId == -1}"
ng-click="switchVersion(-1)"><a>latest</a></li>
<li role="separator" class="divider"></li>
<li class="dropdown-header">已发布</li>
<li ng-repeat="version in releaseVersions"
ng-class="{active:configLocation.versionId == version.id}">
<a ng-click="switchVersion(version.id)">{{version.name}}</a>
</li>
</ul>
</div>
<div class="col-md-10">
<div class="panel">
<header class="panel-heading">
<div class="row">
<div class="col-md-6">项目的基本配置</div>
<div class="col-md-6" ng-show="!currentVersionIsRelease">
<p class="text-right">
<a data-toggle="tooltip"
data-placement="top" title="下载配置">
<span class="glyphicon glyphicon-save"
aria-hidden="true"></span>
</a>
&nbsp;
<a data-toggle="tooltip"
data-placement="top" title="添加配置">
<span class="glyphicon glyphicon-plus"
aria-hidden="true"></span>
</a>
</p>
</div>
</div>
</header>
<!--配置列表-->
<div class="panel-body">
<table class="table table-bordered text-center">
<thead>
<tr>
<th>
Key
</th>
<th>
value
</th>
<th ng-show="!currentVersionIsRelease">
备注
</th>
<th ng-show="!currentVersionIsRelease">
最后修改人
</th>
<th ng-show="!currentVersionIsRelease">
最后修改时间
</th>
<th ng-show="!currentVersionIsRelease">
操作
</th>
</tr>
</thead>
<tbody ng-repeat="config in config.defaultClusterConfigs">
<tr>
<td>
{{config.key}}
</td>
<td>
{{config.value}}
</td>
<td ng-show="!currentVersionIsRelease">
{{config.comment}}
</td>
<td ng-show="!currentVersionIsRelease">
{{config.dataChangeLastModifiedBy}}
</td>
<td ng-show="!currentVersionIsRelease">
{{config.dataChangeLastModifiedTime | date: 'yyyy-MM-dd HH:mm:ss'}}
</td>
<td ng-show="!currentVersionIsRelease">
<a data-toggle="tooltip"
data-placement="top" title="修改">
<span class="glyphicon glyphicon-edit"
aria-hidden="true"></span>
</a>
<a data-toggle="tooltip"
data-placement="top" title="删除">
<span class="glyphicon glyphicon-remove"
aria-hidden="true"></span>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!--重写框架的配置-->
<div ng-repeat="overrideAppConfig in config.overrideAppConfigs">
<div class="panel sec-panel">
<header class="panel-heading">
<div class="row">
<div class="col-md-6">重写<a
>{{overrideAppConfig.appId}}</a>的配置
</div>
<div class="col-md-6" ng-show="!currentVersionIsRelease">
<p class="text-right">
<a data-toggle="tooltip"
data-placement="top"
title="下载配置">
<span class="glyphicon glyphicon-save"
aria-hidden="true"></span>
</a>
&nbsp;
<a data-toggle="tooltip"
data-placement="top"
title="添加配置">
<span class="glyphicon glyphicon-plus"
aria-hidden="true"></span>
</a>
</p>
</div>
</div>
</header>
<!--配置列表-->
<div class="panel-body">
<table class="table table-bordered text-center">
<thead>
<tr>
<th>
Key
</th>
<th>
value
</th>
<th ng-show="!currentVersionIsRelease">
备注
</th>
<th ng-show="!currentVersionIsRelease">
最后修改人
</th>
<th ng-show="!currentVersionIsRelease">
最后修改时间
</th>
<th ng-show="!currentVersionIsRelease">
操作
</th>
</tr>
</thead>
<tbody ng-repeat="config in overrideAppConfig.configs">
<tr>
<td>
{{config.key}}
</td>
<td>
{{config.value}}
</td>
<td ng-show="!currentVersionIsRelease">
{{config.comment}}
</td>
<td ng-show="!currentVersionIsRelease">
{{config.dataChangeLastModifiedBy}}
</td>
<td ng-show="!currentVersionIsRelease">
{{config.dataChangeLastModifiedTime | date: 'yyyy-MM-dd HH:mm:ss'}}
</td>
<td ng-show="!currentVersionIsRelease">
<a data-toggle="tooltip"
data-placement="top"
title="修改">
<span class="glyphicon glyphicon-edit"
aria-hidden="true"></span>
</a>
<a data-toggle="tooltip"
data-placement="top"
title="删除">
<span class="glyphicon glyphicon-remove"
aria-hidden="true"></span>
</a>
</td>
</tr>
</tbody>
</table>
<br>
</div>
</div>
</div>
<!--集群特有的配置-->
<div class="panel" ng-show="showClusterConfigs">
<header class="panel-heading">
<div class="row">
<div class="col-md-1 text-right">
集群
</div>
<div class="col-md-3">
<select class="form-control"
ng-model="config.selectedCluster"
ng-change="selectCluster()"
ng-options="item as item.clusterName for item in config.overrideClusterConfigs track by item.clusterName">
</select>
</div>
<div class="col-md-2 text-left">
</div>
<div class="col-md-6" ng-show="!currentVersionIsRelease">
<p class="text-right">
<a data-toggle="tooltip"
data-placement="top" title="下载配置">
<span class="glyphicon glyphicon-save"
aria-hidden="true"></span>
</a>
&nbsp;
<a data-toggle="tooltip"
data-placement="top" title="添加配置">
<span class="glyphicon glyphicon-plus"
aria-hidden="true"></span>
</a>
</p>
</div>
</div>
</header>
<!--配置列表-->
<div class="panel-body">
<table class="table table-bordered text-center">
<thead>
<tr>
<th>
Key
</th>
<th>
value
</th>
<th ng-show="!currentVersionIsRelease">
备注
</th>
<th ng-show="!currentVersionIsRelease">
最后修改人
</th>
<th ng-show="!currentVersionIsRelease">
最后修改时间
</th>
<th ng-show="!currentVersionIsRelease">
操作
</th>
</tr>
</thead>
<tbody ng-repeat="config in config.selectedClusterKVs">
<tr>
<td>
{{config.key}}
</td>
<td>
{{config.value}}
</td>
<td ng-show="!currentVersionIsRelease">
{{config.comment}}
</td>
<td ng-show="!currentVersionIsRelease">
{{config.dataChangeLastModifiedBy}}
</td>
<td ng-show="!currentVersionIsRelease">
{{config.dataChangeLastModifiedTime | date: 'yyyy-MM-dd HH:mm:ss'}}
</td>
<td ng-show="!currentVersionIsRelease">
<a data-toggle="tooltip"
data-placement="top"
title="修改">
<span class="glyphicon glyphicon-edit"
aria-hidden="true"></span>
</a>
<a data-toggle="tooltip"
data-placement="top"
title="删除">
<span class="glyphicon glyphicon-remove"
aria-hidden="true"></span>
</a>
</td>
</tr>
</tbody>
</table>
<br>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel">
<div class="panel-body">
<form class="form-horizontal">
<div class="form-group">
<label class="col-sm-2 control-label">应用ID:</label>
<label class="col-sm-3 control-label text-left">
{{app.appId}}
</label>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">应用名称:</label>
<label class="col-sm-3 control-label text-left">
{{app.name}}
</label>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Owner:</label>
<label class="col-sm-3 control-label text-left">
{{app.owner}}
</label>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">手机号:</label>
<label class="col-sm-3 control-label text-left">
{{app.ownerPhone}}
</label>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">邮箱地址:</label>
<label class="col-sm-3 control-label text-left">
{{app.ownerMail}}
</label>
</div>
</form>
</div>
</div>
</div>
</div>
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