Explorar o código

Merge pull request #286 from lepdou/0620_27

commit history & bugfix empty value
Jason Song %!s(int64=8) %!d(string=hai) anos
pai
achega
d4ed153fd8
Modificáronse 26 ficheiros con 568 adicións e 157 borrados
  1. 31 0
      apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/CommitController.java
  2. 35 1
      apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/ItemController.java
  3. 8 2
      apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/ItemSetController.java
  4. 1 1
      apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/Commit.java
  5. 3 2
      apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/CommitRepository.java
  6. 10 6
      apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/CommitService.java
  7. 36 4
      apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ItemSetService.java
  8. 68 0
      apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/utils/ConfigChangeContentBuilder.java
  9. 11 1
      apollo-common/src/main/java/com/ctrip/framework/apollo/common/utils/RequestPrecondition.java
  10. 22 0
      apollo-core/src/main/java/com/ctrip/framework/apollo/core/dto/BaseDTO.java
  11. 54 0
      apollo-core/src/main/java/com/ctrip/framework/apollo/core/dto/CommitDTO.java
  12. 20 2
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/api/AdminServiceAPI.java
  13. 2 1
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/AppController.java
  14. 35 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/CommitController.java
  15. 2 2
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConfigController.java
  16. 2 2
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/NamespaceController.java
  17. 1 1
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PermissionController.java
  18. 2 2
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ServerConfigController.java
  19. 0 8
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/form/NamespaceTextModel.java
  20. 23 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/CommitService.java
  21. 114 70
      apollo-portal/src/main/resources/static/config.html
  22. BIN=BIN
      apollo-portal/src/main/resources/static/img/plus.png
  23. BIN=BIN
      apollo-portal/src/main/resources/static/img/release.png
  24. 45 21
      apollo-portal/src/main/resources/static/scripts/controller/app/ConfigNamespaceController.js
  25. 27 0
      apollo-portal/src/main/resources/static/scripts/services/CommitService.js
  26. 16 31
      apollo-portal/src/main/resources/static/styles/common-style.css

+ 31 - 0
apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/CommitController.java

@@ -0,0 +1,31 @@
+package com.ctrip.framework.apollo.adminservice.controller;
+
+import com.ctrip.framework.apollo.biz.entity.Commit;
+import com.ctrip.framework.apollo.biz.service.CommitService;
+import com.ctrip.framework.apollo.common.utils.BeanUtils;
+import com.ctrip.framework.apollo.core.dto.CommitDTO;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Pageable;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+
+@RestController
+public class CommitController {
+
+  @Autowired
+  private CommitService commitService;
+
+  @RequestMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/commit")
+  public List<CommitDTO> find(@PathVariable String appId, @PathVariable String clusterName,
+                              @PathVariable String namespaceName, Pageable pageable){
+
+    List<Commit> commits = commitService.find(appId, clusterName, namespaceName, pageable);
+    return BeanUtils.batchTransform(CommitDTO.class, commits);
+  }
+
+}

+ 35 - 1
apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/ItemController.java

@@ -11,8 +11,13 @@ import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
+import com.ctrip.framework.apollo.biz.entity.Commit;
 import com.ctrip.framework.apollo.biz.entity.Item;
+import com.ctrip.framework.apollo.biz.entity.Namespace;
+import com.ctrip.framework.apollo.biz.service.CommitService;
 import com.ctrip.framework.apollo.biz.service.ItemService;
+import com.ctrip.framework.apollo.biz.service.NamespaceService;
+import com.ctrip.framework.apollo.biz.utils.ConfigChangeContentBuilder;
 import com.ctrip.framework.apollo.common.utils.BeanUtils;
 import com.ctrip.framework.apollo.core.dto.ItemDTO;
 import com.ctrip.framework.apollo.core.exception.NotFoundException;
@@ -22,16 +27,24 @@ public class ItemController {
 
   @Autowired
   private ItemService itemService;
+  @Autowired
+  private NamespaceService namespaceService;
+  @Autowired
+  private CommitService commitService;
 
   @RequestMapping(path = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items", method = RequestMethod.POST)
   public ItemDTO createOrUpdate(@PathVariable("appId") String appId,
                                 @PathVariable("clusterName") String clusterName,
                                 @PathVariable("namespaceName") String namespaceName, @RequestBody ItemDTO dto) {
     Item entity = BeanUtils.transfrom(Item.class, dto);
+
+    ConfigChangeContentBuilder builder = new ConfigChangeContentBuilder();
     Item managedEntity = itemService.findOne(appId, clusterName, namespaceName, entity.getKey());
     if (managedEntity != null) {
+      Item beforeUpdateItem = BeanUtils.transfrom(Item.class, managedEntity);
       BeanUtils.copyEntityProperties(entity, managedEntity);
       entity = itemService.update(managedEntity);
+      builder.updateItem(beforeUpdateItem, entity);
     } else {
       Item lastItem = itemService.findLastOne(appId, clusterName, namespaceName);
       int lineNum = 1;
@@ -41,9 +54,19 @@ public class ItemController {
       }
       entity.setLineNum(lineNum);
       entity = itemService.save(entity);
+      builder.createItem(entity);
     }
-
     dto = BeanUtils.transfrom(ItemDTO.class, entity);
+
+    Commit commit = new Commit();
+    commit.setAppId(appId);
+    commit.setClusterName(clusterName);
+    commit.setNamespaceName(namespaceName);
+    commit.setChangeSets(builder.build());
+    commit.setDataChangeCreatedBy(dto.getDataChangeLastModifiedBy());
+    commit.setDataChangeLastModifiedBy(dto.getDataChangeLastModifiedBy());
+    commitService.save(commit);
+
     return dto;
   }
 
@@ -54,6 +77,17 @@ public class ItemController {
       throw new NotFoundException("item not found for itemId " + itemId);
     }
     itemService.delete(entity.getId(), operator);
+
+    Namespace namespace = namespaceService.findOne(entity.getNamespaceId());
+
+    Commit commit = new Commit();
+    commit.setAppId(namespace.getAppId());
+    commit.setClusterName(namespace.getClusterName());
+    commit.setNamespaceName(namespace.getNamespaceName());
+    commit.setChangeSets(new ConfigChangeContentBuilder().deleteItem(entity).build());
+    commit.setDataChangeCreatedBy(operator);
+    commit.setDataChangeLastModifiedBy(operator);
+    commitService.save(commit);
   }
 
   @RequestMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items")

+ 8 - 2
apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/ItemSetController.java

@@ -3,6 +3,7 @@ package com.ctrip.framework.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,13 @@ 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();
   }
+
+
 }

+ 1 - 1
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/Commit.java

@@ -15,7 +15,7 @@ import javax.persistence.Table;
 @Where(clause = "isDeleted = 0")
 public class Commit extends BaseEntity {
 
-  @Column(name = "ChangeSets", nullable = false)
+  @Column(name = "ChangeSets", length = 4048, nullable = false)
   private String changeSets;
 
   @Column(name = "AppId", nullable = false)

+ 3 - 2
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/repository/CommitRepository.java

@@ -2,13 +2,14 @@ package com.ctrip.framework.apollo.biz.repository;
 
 import com.ctrip.framework.apollo.biz.entity.Commit;
 
+import org.springframework.data.domain.Pageable;
 import org.springframework.data.repository.PagingAndSortingRepository;
 
 import java.util.List;
 
 public interface CommitRepository extends PagingAndSortingRepository<Commit, Long> {
 
-  List<Commit> findByAppIdAndClusterNameAndNamespaceName(String appId, String clusterName,
-                                                         String namespaceName);
+  List<Commit> findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(String appId, String clusterName,
+                                                         String namespaceName, Pageable pageable);
 
 }

+ 10 - 6
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/CommitService.java

@@ -4,9 +4,11 @@ import com.ctrip.framework.apollo.biz.entity.Commit;
 import com.ctrip.framework.apollo.biz.repository.CommitRepository;
 
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
-import java.util.Date;
+import java.util.List;
 
 @Service
 public class CommitService {
@@ -14,12 +16,14 @@ public class CommitService {
   @Autowired
   private CommitRepository commitRepository;
 
-  public void save(Commit commit, String user){
-
+  @Transactional
+  public Commit save(Commit commit){
     commit.setId(0);//protection
-    commit.setDataChangeCreatedBy(user);
-    commit.setDataChangeCreatedTime(new Date());
-    commitRepository.save(commit);
+    return commitRepository.save(commit);
+  }
+
+  public List<Commit> find(String appId, String clusterName, String namespaceName, Pageable page){
+    return commitRepository.findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(appId, clusterName, namespaceName, page);
   }
 
 }

+ 36 - 4
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/ItemSetService.java

@@ -6,12 +6,15 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 
 import com.ctrip.framework.apollo.biz.entity.Audit;
+import com.ctrip.framework.apollo.biz.entity.Commit;
 import com.ctrip.framework.apollo.biz.entity.Item;
 import com.ctrip.framework.apollo.biz.repository.ItemRepository;
+import com.ctrip.framework.apollo.biz.utils.ConfigChangeContentBuilder;
 import com.ctrip.framework.apollo.common.utils.BeanUtils;
 import com.ctrip.framework.apollo.core.dto.ItemChangeSets;
 import com.ctrip.framework.apollo.core.dto.ItemDTO;
 
+
 @Service
 public class ItemSetService {
 
@@ -21,16 +24,24 @@ public class ItemSetService {
   @Autowired
   private AuditService auditService;
 
+  @Autowired
+  private CommitService commitService;
+
+
   @Transactional
-  public void updateSet(ItemChangeSets changeSet) {
+  public ItemChangeSets updateSet(String appId, String clusterName,
+                                  String namespaceName, ItemChangeSets changeSet) {
     String operator = changeSet.getDataChangeLastModifiedBy();
+    ConfigChangeContentBuilder configChangeContentBuilder = new ConfigChangeContentBuilder();
+
     if (!CollectionUtils.isEmpty(changeSet.getCreateItems())) {
       for (ItemDTO item : changeSet.getCreateItems()) {
         Item entity = BeanUtils.transfrom(Item.class, item);
         entity.setId(0);//protection
         entity.setDataChangeCreatedBy(operator);
         entity.setDataChangeLastModifiedBy(operator);
-        itemRepository.save(entity);
+        Item createdItem = itemRepository.save(entity);
+        configChangeContentBuilder.createItem(createdItem);
       }
       auditService.audit("ItemSet", null, Audit.OP.INSERT, operator);
     }
@@ -39,9 +50,12 @@ public class ItemSetService {
       for (ItemDTO item : changeSet.getUpdateItems()) {
         Item entity = BeanUtils.transfrom(Item.class, item);
         Item managedItem = itemRepository.findOne(entity.getId());
+        Item beforeUpdateItem = BeanUtils.transfrom(Item.class, managedItem);
         BeanUtils.copyEntityProperties(entity, managedItem);
         managedItem.setDataChangeLastModifiedBy(operator);
-        itemRepository.save(managedItem);
+        Item updatedItem = itemRepository.save(managedItem);
+        configChangeContentBuilder.updateItem(beforeUpdateItem, updatedItem);
+
       }
       auditService.audit("ItemSet", null, Audit.OP.UPDATE, operator);
     }
@@ -51,9 +65,27 @@ public class ItemSetService {
         Item entity = BeanUtils.transfrom(Item.class, item);
         entity.setDeleted(true);
         entity.setDataChangeLastModifiedBy(operator);
-        itemRepository.save(entity);
+        Item deletedItem = itemRepository.save(entity);
+        configChangeContentBuilder.deleteItem(deletedItem);
       }
       auditService.audit("ItemSet", null, Audit.OP.DELETE, operator);
     }
+
+    createCommit(appId, clusterName, namespaceName, configChangeContentBuilder.build(), changeSet.getDataChangeLastModifiedBy());
+    return changeSet;
+
+  }
+
+  private void createCommit(String appId, String clusterName, String namespaceName, String configChangeContent,  String operator){
+
+    Commit commit = new Commit();
+    commit.setAppId(appId);
+    commit.setClusterName(clusterName);
+    commit.setNamespaceName(namespaceName);
+    commit.setChangeSets(configChangeContent);
+    commit.setDataChangeCreatedBy(operator);
+    commit.setDataChangeLastModifiedBy(operator);
+    commitService.save(commit);
   }
+
 }

+ 68 - 0
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/utils/ConfigChangeContentBuilder.java

@@ -0,0 +1,68 @@
+package com.ctrip.framework.apollo.biz.utils;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import com.ctrip.framework.apollo.biz.entity.Item;
+import com.ctrip.framework.apollo.common.utils.BeanUtils;
+import com.ctrip.framework.apollo.core.dto.ItemChangeSets;
+import com.ctrip.framework.apollo.core.dto.ItemDTO;
+
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+
+public class ConfigChangeContentBuilder {
+
+  private static final Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create();
+
+  private List<Item> createItems = new LinkedList<>();
+  private List<ItemPair> updateItems = new LinkedList<>();
+  private List<Item> deleteItems = new LinkedList<>();
+
+
+  public ConfigChangeContentBuilder createItem(Item item) {
+    createItems.add(item);
+    return this;
+  }
+
+  public ConfigChangeContentBuilder updateItem(Item oldItem, Item newItem) {
+    ItemPair itemPair = new ItemPair(oldItem, newItem);
+    updateItems.add(itemPair);
+    return this;
+  }
+
+  public ConfigChangeContentBuilder deleteItem(Item item) {
+    deleteItems.add(item);
+    return this;
+  }
+
+  public String build() {
+    //因为事务第一段提交并没有更新时间,所以build时统一更新
+    for (Item item : createItems) {
+      item.setDataChangeLastModifiedTime(new Date());
+    }
+
+    for (ItemPair item : updateItems) {
+      item.newItem.setDataChangeLastModifiedTime(new Date());
+    }
+
+    for (Item item : deleteItems) {
+      item.setDataChangeLastModifiedTime(new Date());
+    }
+    return gson.toJson(this);
+  }
+
+  class ItemPair {
+
+    Item oldItem;
+    Item newItem;
+
+    public ItemPair(Item oldItem, Item newItem) {
+      this.oldItem = oldItem;
+      this.newItem = newItem;
+    }
+  }
+
+}

+ 11 - 1
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/util/RequestPrecondition.java → apollo-common/src/main/java/com/ctrip/framework/apollo/common/utils/RequestPrecondition.java

@@ -1,4 +1,4 @@
-package com.ctrip.framework.apollo.portal.util;
+package com.ctrip.framework.apollo.common.utils;
 
 
 import com.ctrip.framework.apollo.core.exception.BadRequestException;
@@ -10,6 +10,8 @@ public class RequestPrecondition {
 
   private static String ILLEGAL_MODEL = "request model is invalid";
 
+  private static String ILLEGAL_NUMBER = "number should be positive";
+
   public static void checkArgument(String... args) {
     checkArgument(!StringUtils.isContainEmpty(args), CONTAIN_EMPTY_ARGUMENT);
   }
@@ -24,6 +26,14 @@ public class RequestPrecondition {
     }
   }
 
+  public static void checkNumberPositive(int... args){
+    for (int num: args){
+      if (num <= 0){
+        throw new BadRequestException(ILLEGAL_NUMBER);
+      }
+    }
+  }
+
 
 
 }

+ 22 - 0
apollo-core/src/main/java/com/ctrip/framework/apollo/core/dto/BaseDTO.java

@@ -1,12 +1,18 @@
 package com.ctrip.framework.apollo.core.dto;
 
 
+import java.util.Date;
+
 public class BaseDTO {
 
   protected String dataChangeCreatedBy;
 
   protected String dataChangeLastModifiedBy;
 
+  protected Date dataChangeCreatedTime;
+
+  protected Date dataChangeLastModifiedTime;
+
   public String getDataChangeCreatedBy() {
     return dataChangeCreatedBy;
   }
@@ -22,4 +28,20 @@ public class BaseDTO {
   public void setDataChangeLastModifiedBy(String dataChangeLastModifiedBy) {
     this.dataChangeLastModifiedBy = dataChangeLastModifiedBy;
   }
+
+  public Date getDataChangeCreatedTime() {
+    return dataChangeCreatedTime;
+  }
+
+  public void setDataChangeCreatedTime(Date dataChangeCreatedTime) {
+    this.dataChangeCreatedTime = dataChangeCreatedTime;
+  }
+
+  public Date getDataChangeLastModifiedTime() {
+    return dataChangeLastModifiedTime;
+  }
+
+  public void setDataChangeLastModifiedTime(Date dataChangeLastModifiedTime) {
+    this.dataChangeLastModifiedTime = dataChangeLastModifiedTime;
+  }
 }

+ 54 - 0
apollo-core/src/main/java/com/ctrip/framework/apollo/core/dto/CommitDTO.java

@@ -0,0 +1,54 @@
+package com.ctrip.framework.apollo.core.dto;
+
+public class CommitDTO extends BaseDTO{
+
+  private String changeSets;
+
+  private String appId;
+
+  private String clusterName;
+
+  private String namespaceName;
+
+  private String comment;
+
+  public String getChangeSets() {
+    return changeSets;
+  }
+
+  public void setChangeSets(String changeSets) {
+    this.changeSets = changeSets;
+  }
+
+  public String getAppId() {
+    return appId;
+  }
+
+  public void setAppId(String appId) {
+    this.appId = appId;
+  }
+
+  public String getClusterName() {
+    return clusterName;
+  }
+
+  public void setClusterName(String clusterName) {
+    this.clusterName = clusterName;
+  }
+
+  public String getNamespaceName() {
+    return namespaceName;
+  }
+
+  public void setNamespaceName(String namespaceName) {
+    this.namespaceName = namespaceName;
+  }
+
+  public String getComment() {
+    return comment;
+  }
+
+  public void setComment(String comment) {
+    this.comment = comment;
+  }
+}

+ 20 - 2
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/api/AdminServiceAPI.java

@@ -2,6 +2,7 @@ package com.ctrip.framework.apollo.portal.api;
 
 
 import com.ctrip.framework.apollo.core.dto.AppNamespaceDTO;
+import com.ctrip.framework.apollo.core.dto.CommitDTO;
 import com.ctrip.framework.apollo.core.enums.Env;
 import com.ctrip.framework.apollo.core.dto.AppDTO;
 import com.ctrip.framework.apollo.core.dto.ClusterDTO;
@@ -66,8 +67,10 @@ public class AdminServiceAPI {
 
     public NamespaceDTO loadNamespace(String appId, Env env, String clusterName,
                                       String namespaceName) {
-      NamespaceDTO dto = restTemplate.getForObject("{host}/apps/{appId}/clusters/{clusterName}/namespaces/" + namespaceName,
-                                       NamespaceDTO.class, getAdminServiceHost(env), appId, clusterName);
+      NamespaceDTO
+          dto =
+          restTemplate.getForObject("{host}/apps/{appId}/clusters/{clusterName}/namespaces/" + namespaceName,
+                                    NamespaceDTO.class, getAdminServiceHost(env), appId, clusterName);
       return dto;
     }
 
@@ -157,4 +160,19 @@ public class AdminServiceAPI {
     }
   }
 
+  @Service
+  public static class CommitAPI extends API {
+
+    public List<CommitDTO> find(String appId, Env env, String clusterName, String namespaceName, int page, int size) {
+
+      CommitDTO[]
+          commitDTOs =
+          restTemplate.getForObject("{host}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/commit?page={page}&size={size}",
+                                    CommitDTO[].class,
+                                    getAdminServiceHost(env), appId, clusterName, namespaceName, page, size);
+
+      return Arrays.asList(commitDTOs);
+    }
+  }
+
 }

+ 2 - 1
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/AppController.java

@@ -25,7 +25,7 @@ import org.springframework.web.client.HttpClientErrorException;
 
 import java.util.List;
 
-import static com.ctrip.framework.apollo.portal.util.RequestPrecondition.checkArgument;
+import static com.ctrip.framework.apollo.common.utils.RequestPrecondition.checkArgument;
 
 @RestController
 @RequestMapping("/apps")
@@ -40,6 +40,7 @@ public class AppController {
   @Autowired
   private ApplicationEventPublisher publisher;
 
+
   @RequestMapping("")
   public List<App> findAllApp() {
     return appService.findAll();

+ 35 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/CommitController.java

@@ -0,0 +1,35 @@
+package com.ctrip.framework.apollo.portal.controller;
+
+import com.ctrip.framework.apollo.core.dto.CommitDTO;
+import com.ctrip.framework.apollo.core.enums.Env;
+import com.ctrip.framework.apollo.portal.service.CommitService;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+
+@RestController
+public class CommitController {
+
+  @Autowired
+  private CommitService commitService;
+
+  @RequestMapping(value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/commits")
+  public List<CommitDTO> find(@PathVariable String appId, @PathVariable String env,
+                              @PathVariable String clusterName, @PathVariable String namespaceName,
+                              @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size){
+
+    if (page < 0){
+      page = 0;
+    }
+
+    return commitService.find(appId, Env.valueOf(env), clusterName, namespaceName, page, size);
+
+  }
+
+}

+ 2 - 2
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConfigController.java

@@ -25,7 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
 
 import java.util.List;
 
-import static com.ctrip.framework.apollo.portal.util.RequestPrecondition.checkModel;
+import static com.ctrip.framework.apollo.common.utils.RequestPrecondition.checkModel;
 
 @RestController
 @RequestMapping("")
@@ -128,7 +128,7 @@ public class ConfigController {
   }
 
   private boolean isValidItem(ItemDTO item){
-    return item != null && !StringUtils.isContainEmpty(item.getKey(), item.getValue());
+    return item != null && !StringUtils.isContainEmpty(item.getKey());
   }
 
 }

+ 2 - 2
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/NamespaceController.java

@@ -29,8 +29,8 @@ import org.springframework.web.bind.annotation.RestController;
 
 import java.util.List;
 
-import static com.ctrip.framework.apollo.portal.util.RequestPrecondition.checkArgument;
-import static com.ctrip.framework.apollo.portal.util.RequestPrecondition.checkModel;
+import static com.ctrip.framework.apollo.common.utils.RequestPrecondition.checkArgument;
+import static com.ctrip.framework.apollo.common.utils.RequestPrecondition.checkModel;
 
 @RestController
 public class NamespaceController {

+ 1 - 1
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PermissionController.java

@@ -24,7 +24,7 @@ import org.springframework.web.bind.annotation.RestController;
 
 import java.util.Set;
 
-import static com.ctrip.framework.apollo.portal.util.RequestPrecondition.checkArgument;
+import static com.ctrip.framework.apollo.common.utils.RequestPrecondition.checkArgument;
 
 
 @RestController

+ 2 - 2
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ServerConfigController.java

@@ -12,8 +12,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.bind.annotation.RestController;
 
-import static com.ctrip.framework.apollo.portal.util.RequestPrecondition.checkArgument;
-import static com.ctrip.framework.apollo.portal.util.RequestPrecondition.checkModel;
+import static com.ctrip.framework.apollo.common.utils.RequestPrecondition.checkArgument;
+import static com.ctrip.framework.apollo.common.utils.RequestPrecondition.checkModel;
 
 /**
  * 配置中心本身需要一些配置,这些配置放在数据库里面

+ 0 - 8
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/form/NamespaceTextModel.java

@@ -12,7 +12,6 @@ public class NamespaceTextModel implements Verifiable {
   private String namespaceName;
   private int namespaceId;
   private String configText;
-  private String comment;
 
   @Override
   public boolean isInvalid(){
@@ -66,11 +65,4 @@ public class NamespaceTextModel implements Verifiable {
     this.configText = configText;
   }
 
-  public String getComment() {
-    return comment;
-  }
-
-  public void setComment(String comment) {
-    this.comment = comment;
-  }
 }

+ 23 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/CommitService.java

@@ -0,0 +1,23 @@
+package com.ctrip.framework.apollo.portal.service;
+
+import com.ctrip.framework.apollo.core.dto.CommitDTO;
+import com.ctrip.framework.apollo.core.enums.Env;
+import com.ctrip.framework.apollo.portal.api.AdminServiceAPI;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class CommitService {
+
+
+  @Autowired
+  private AdminServiceAPI.CommitAPI commitAPI;
+
+  public List<CommitDTO> find(String appId, Env env, String clusterName, String namespaceName, int page, int size){
+    return commitAPI.find(appId, env, clusterName, namespaceName, page, size);
+  }
+
+}

+ 114 - 70
apollo-portal/src/main/resources/static/config.html

@@ -120,7 +120,7 @@
                             </div>
                             <div class="col-md-8 text-right">
                                 <button type="button"
-                                        class="btn btn-default btn-sm J_tableview_btn"
+                                        class="btn btn-success btn-sm J_tableview_btn"
                                         ng-disabled="namespace.isTextEditing"
                                         ng-click="prepareReleaseNamespace(namespace)">
                                     <img src="img/release.png">
@@ -195,11 +195,11 @@
                                 <a data-tooltip="tooltip" data-placement="bottom" title="提交修改"
                                    data-toggle="modal" data-target="#commitModal"
                                    ng-show="namespace.isTextEditing && namespace.viewType == 'text'"
-                                   ng-click="setCommitNamespace(namespace)">
+                                   ng-click="commitChange(namespace)">
                                     <img src="img/submit.png" class="ns_btn">
                                 </a>
 
-                                <button type="button" class="btn btn-default btn-sm"
+                                <button type="button" class="btn btn-primary btn-sm"
                                         ng-show="namespace.viewType == 'table' && namespace.hasModifyPermission"
                                         data-toggle="modal" data-target="#itemModal"
                                         ng-click="createItem(namespace)">
@@ -234,7 +234,7 @@
                                     Key
                                 </th>
                                 <th>
-                                    value
+                                    Value
                                 </th>
                                 <th>
                                     备注
@@ -277,7 +277,8 @@
                                          data-toggle="modal" data-target="#itemModal"
                                          ng-click="editItem(namespace, config.item)"
                                          ng-show="namespace.hasModifyPermission">
-                                    <img style="margin-left: 5px;" src="img/cancel.png" data-toggle="modal" data-target="#deleteConfirmDialog"
+                                    <img style="margin-left: 5px;" src="img/cancel.png" data-toggle="modal"
+                                         data-target="#deleteConfirmDialog"
                                          data-tooltip="tooltip" data-placement="bottom" title="删除"
                                          ng-click="preDeleteItem(namespace, config.item.id)"
                                          ng-show="namespace.hasModifyPermission">
@@ -289,72 +290,114 @@
                     </div>
 
                     <!--历史修改视图-->
-                    <div class="J_historyview history-view"
-                         ng-show="namespace.viewType == 'history'">
-                        <div class="row">
-                            <div class="col-md-11 col-md-offset-1 list" style="">
-                                <div class="media">
-                                    <img src="img/history.png"/>
-
-                                    <div class="row">
-                                        <div class="col-md-10"><h5 class="media-heading">2016-02-23
-                                            12:23
-                                            王玉</h5>
-
-                                            <p>
-                                                修改comment
-                                            </p></div>
-                                        <div class="col-md-2 text-right">
-                                            <button class="btn btn-default" type="submit">查看修改内容
-                                            </button>
-                                        </div>
-                                    </div>
-                                    <hr>
-                                </div>
-                                <div class="media">
-                                    <img src="img/history.png"/>
-
-                                    <div class="row">
-                                        <div class="col-md-10"><h5 class="media-heading">2016-02-23
-                                            12:23
-                                            王玉</h5>
-
-                                            <p>
-                                                修改comment
-                                            </p></div>
-                                        <div class="col-md-2 text-right">
-                                            <button class="btn btn-default" type="submit">查看修改内容
-                                            </button>
-                                        </div>
-                                    </div>
-                                </div>
-                            </div>
+                    <div class="J_historyview history-view" ng-show="namespace.viewType == 'history'">
+                        <div class="text-right">
+                            <span class="label label-primary change-type-mark">&nbsp;</span>
+                            <small>新增&nbsp;</small>
+                            <span class="label label-info change-type-mark">&nbsp;</span>
+                            <small>更新&nbsp;</small>
+                            <span class="label label-danger change-type-mark">&nbsp;</span>
+                            <small>删除&nbsp;</small>
                         </div>
 
-                    </div>
-                </div>
-            </div>
-
-            <!-- commit modify config modal-->
-            <div class="modal fade" id="commitModal" tabindex="-1" role="dialog">
-                <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">Commit changes</h4>
-                        </div>
-                        <div class="modal-body">
-                        <textarea rows="4" class="form-control" style="width:570px;"
-                                  placeholder="Add an optional extended description..."
-                                  ng-model="commitComment"></textarea>
+                        <div class="media" ng-repeat="commits in namespace.commits">
+                            <div class="media-left">
+                                <h4 class="media-heading" ng-bind="commits.dataChangeCreatedBy"></h4>
+                            </div>
+                            <div class="media-body">
+                                <h5 class="media-heading"
+                                    ng-bind="commits.dataChangeCreatedTime  | date: 'yyyy-MM-dd HH:mm:ss'"></h5>
+                                <table class="table table-bordered table-striped text-center table-hover">
+                                    <thead>
+                                    <tr>
+                                        <th>
+                                            Type
+                                        </th>
+                                        <th>
+                                            Key
+                                        </th>
+                                        <th>
+                                            Old Value
+                                        </th>
+                                        <th>
+                                            New Value
+                                        </th>
+                                        <th>
+                                            Comment
+                                        </th>
+                                    </tr>
+                                    </thead>
+                                    <tbody>
+
+                                    <tr ng-repeat="item in commits.changeSets.createItems" ng-show="item.key || item.comment">
+                                        <td width="2%">
+                                            <span class="label label-primary change-type-mark">&nbsp;</span>
+                                        </td>
+                                        <td width="20%" title="{{item.key}}">
+
+                                            <span ng-bind="item.key | limitTo: 250"></span>
+                                            <span ng-bind="item.key.length > 250 ? '...' :''"></span>
+                                        </td>
+                                        <td width="30%">
+                                        </td>
+                                        <td width="30%" title="{{item.value}}">
+                                            <span ng-bind="item.value | limitTo: 250"></span>
+                                            <span ng-bind="item.value.length > 250 ? '...': ''"></span>
+                                        </td>
+                                        <td width="18%" title="{{item.comment}}">
+                                            <span ng-bind="item.comment | limitTo: 250"></span>
+                                            <span ng-bind="item.comment.length > 250 ?'...' : ''"></span>
+                                        </td>
+                                    </tr>
+                                    <tr ng-repeat="item in commits.changeSets.updateItems">
+                                        <td width="2%">
+                                            <span class="label label-info change-type-mark">&nbsp;</span>
+                                        </td>
+                                        <td width="20%" title="{{item.newItem.key}}">
+                                            <span ng-bind="item.newItem.key | limitTo: 250"></span>
+                                            <span ng-bind="item.newItem.key.length > 250 ? '...' :''"></span>
+                                        </td>
+                                        <td width="30%" title="{{item.oldItem.value}}">
+                                            <span ng-bind="item.oldItem.value | limitTo: 250"></span>
+                                            <span ng-bind="item.oldItem.value.length > 250 ? '...': ''"></span>
+                                        </td>
+                                        <td width="30%" title="{{item.newItem.value}}">
+                                            <span ng-bind="item.newItem.value | limitTo: 250"></span>
+                                            <span ng-bind="item.newItem.value.length > 250 ? '...': ''"></span>
+                                        </td>
+                                        <td width="18%" title="{{item.newItem.comment}}">
+                                            <span ng-bind="item.newItem.comment | limitTo: 250"></span>
+                                            <span ng-bind="item.newItem.comment.length > 250 ?'...' : ''"></span>
+                                        </td>
+                                    </tr>
+                                    <tr ng-repeat="item in commits.changeSets.deleteItems" ng-show="item.key || item.comment">
+                                        <td width="2%">
+                                            <span class="label label-danger change-type-mark">&nbsp;</span>
+                                        </td>
+                                        <td width="20%" title="{{item.key}}">
+                                            <span ng-bind="item.key | limitTo: 250"></span>
+                                            <span ng-bind="item.key.length > 250 ? '...' :''"></span>
+                                        </td>
+                                        <td width="30%">
+                                        </td>
+                                        <td width="30%" title="{{item.value}}">
+                                            <span ng-bind="item.value | limitTo: 250"></span>
+                                            <span ng-bind="item.value.length > 250 ? '...': ''"></span>
+                                        </td>
+                                        <td width="18%" title="{{item.comment}}">
+                                            <span ng-bind="item.comment | limitTo: 250"></span>
+                                            <span ng-bind="item.comment.length > 250 ?'...' : ''"></span>
+                                        </td>
+                                    </tr>
+                                    </tbody>
+                                </table>
+                            </div>
+                            <hr>
                         </div>
-                        <div class="modal-footer">
-                            <button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
-                            <button type="button" class="btn btn-primary" data-dismiss="modal"
-                                    ng-click="commitChange()">
-                                提交
-                            </button>
+                        <div class="text-center">
+                            <button type="button" class="btn btn-default" ng-show="!namespace.hasLoadAllCommit"
+                                    ng-click="loadCommitHistory(namespace)">加载更多
+                                <span class="glyphicon glyphicon-menu-down"></span></button>
                         </div>
                     </div>
                 </div>
@@ -410,7 +453,7 @@
                                             ng-if="config.item.key && config.isModified">
                                             <td width="20%" title="{{config.item.key}}">
                                                         <span class="label label-danger"
-                                                              ng-show="!config.newValue">deleted</span>
+                                                              ng-show="!config.item.lastModifiedBy">deleted</span>
                                                 <span ng-bind="config.item.key | limitTo: 250"></span>
                                                 <span ng-bind="config.item.key.length > 250 ? '...' :''"></span>
                                             </td>
@@ -497,7 +540,7 @@
                                 </label>
                                 <div class="col-sm-10">
                                             <textarea type="text" class="form-control" rows="6" ng-model="item.value"
-                                                      ng-required="true" ng-show="tableViewOperType != 'retrieve'">
+                                                       ng-show="tableViewOperType != 'retrieve'">
                                                 </textarea>
                                     <p ng-bind="item.value" ng-show="tableViewOperType == 'retrieve'"></p>
                                 </div>
@@ -586,6 +629,7 @@
 <script type="application/javascript" src="scripts/services/UserService.js"></script>
 <script type="application/javascript" src="scripts/services/ConfigService.js"></script>
 <script type="application/javascript" src="scripts/services/PermissionService.js"></script>
+<script type="application/javascript" src="scripts/services/CommitService.js"></script>
 
 
 <script type="application/javascript" src="scripts/AppUtils.js"></script>

BIN=BIN
apollo-portal/src/main/resources/static/img/plus.png


BIN=BIN
apollo-portal/src/main/resources/static/img/release.png


+ 45 - 21
apollo-portal/src/main/resources/static/scripts/controller/app/ConfigNamespaceController.js

@@ -1,6 +1,7 @@
 application_module.controller("ConfigNamespaceController",
                               ['$rootScope', '$scope', '$location', 'toastr', 'AppUtil', 'ConfigService', 'PermissionService',
-                               function ($rootScope, $scope, $location, toastr, AppUtil, ConfigService, PermissionService) {
+                               'CommitService',
+                               function ($rootScope, $scope, $location, toastr, AppUtil, ConfigService, PermissionService, CommitService) {
 
                                    var namespace_view_type = {
                                        TEXT: 'text',
@@ -78,14 +79,41 @@ application_module.controller("ConfigNamespaceController",
                                            
                                        });
                                    $scope.switchView = function (namespace, viewType) {
+                                       namespace.viewType = viewType;
                                        if (namespace_view_type.TEXT == viewType) {
                                            namespace.text = parseModel2Text(namespace);
                                        } else if (namespace_view_type.TABLE == viewType) {
 
+                                       } else {
+                                           $scope.loadCommitHistory(namespace);
                                        }
-                                       namespace.viewType = viewType;
                                    };
 
+                                   $scope.loadCommitHistory = function(namespace) {
+                                       if (!namespace.commits){
+                                           namespace.commits = [];
+                                           namespace.commitPage = 0;
+                                       }
+                                       CommitService.find_commits($rootScope.pageContext.appId,
+                                                                  $rootScope.pageContext.env,
+                                                                  $rootScope.pageContext.clusterName,
+                                                                  namespace.namespace.namespaceName,
+                                                                  namespace.commitPage)
+                                           .then(function (result) {
+                                               if (result.length == 0){
+                                                   namespace.hasLoadAllCommit = true;
+                                               }
+                                               for (var i = 0; i < result.length; i++) {
+                                                   //to json
+                                                   result[i].changeSets = JSON.parse(result[i].changeSets);
+                                                   namespace.commits.push(result[i]);
+                                               }
+                                               namespace.commitPage += 1;
+                                           }, function (result) {
+                                               toastr.error(AppUtil.errorMsg(result), "加载修改历史记录出错");
+                                           });
+                                   }
+
                                    var MAX_ROW_SIZE = 30;
                                    var APPEND_ROW_SIZE = 8;
                                    //把表格内容解析成文本
@@ -118,28 +146,20 @@ application_module.controller("ConfigNamespaceController",
                                        return result;
                                    }
 
-                                   $scope.toCommitNamespace = {};
-
-                                   $scope.setCommitNamespace = function (namespace) {
-                                       $scope.toCommitNamespace = namespace;
-                                   };
-
-                                   $scope.commitComment = '';
                                    //更新配置
-                                   $scope.commitChange = function () {
+                                   $scope.commitChange = function (namespace) {
                                        ConfigService.modify_items($scope.pageContext.appId, $scope.pageContext.env,
                                                                   $scope.pageContext.clusterName,
-                                                                  $scope.toCommitNamespace.namespace.namespaceName,
-                                                                  $scope.toCommitNamespace.editText,
-                                                                  $scope.toCommitNamespace.namespace.id,
-                                                                  $scope.commitComment).then(
+                                                                  namespace.namespace.namespaceName,
+                                                                  namespace.editText,
+                                                                  namespace.namespace.id).then(
                                            function (result) {
                                                toastr.success("更新成功");
                                                //refresh all namespace items
                                                $rootScope.refreshNamespaces();
 
-                                               $scope.toCommitNamespace.commited = true;
-                                               $scope.toggleTextEditStatus($scope.toCommitNamespace);
+                                               namespace.commited = true;
+                                               $scope.toggleTextEditStatus(namespace);
 
                                            }, function (result) {
                                                toastr.error(AppUtil.errorMsg(result), "更新失败");
@@ -151,12 +171,12 @@ application_module.controller("ConfigNamespaceController",
                                    $scope.toggleTextEditStatus = function (namespace) {
                                        namespace.isTextEditing = !namespace.isTextEditing;
                                        if (namespace.isTextEditing) {//切换为编辑状态
-                                           $scope.toCommitNamespace.commited = false;
+                                           namespace.commited = false;
                                            namespace.backupText = namespace.text;
                                            namespace.editText = parseModel2Text(namespace, 'edit');
 
                                        } else {
-                                           if (!$scope.toCommitNamespace.commited) {//取消编辑,则复原
+                                           if (!namespace.commited) {//取消编辑,则复原
                                                namespace.text = namespace.backupText;
                                            }
                                        }
@@ -286,15 +306,19 @@ application_module.controller("ConfigNamespaceController",
                                                        });
 
                                                } else if ($scope.tableViewOperType == TABLE_VIEW_OPER_TYPE.UPDATE) {
-
+                                                   if (!$scope.item.value){
+                                                       $scope.item.value = "";
+                                                   }
+                                                   if (!$scope.item.comment){
+                                                       $scope.item.comment = "";
+                                                   }
                                                    ConfigService.update_item($rootScope.pageContext.appId,
                                                                              cluster.env,
                                                                              cluster.name,
                                                                              toOperationNamespaceName,
                                                                              $scope.item).then(
                                                        function (result) {
-                                                           toastr.success("[" + cluster.env + "," + cluster.name + "]",
-                                                                          "更新成功");
+                                                           toastr.success("更新成功, 如需生效请发布");
                                                            itemModal.modal('hide');
                                                            $rootScope.refreshNamespaces(namespace_view_type.TABLE);
                                                        }, function (result) {

+ 27 - 0
apollo-portal/src/main/resources/static/scripts/services/CommitService.js

@@ -0,0 +1,27 @@
+appService.service('CommitService', ['$resource', '$q', function ($resource, $q) {
+    var commit_resource = $resource('', {}, {
+        find_commits: {
+            method: 'GET',
+            isArray: true,
+            url: '/apps/:appId/envs/:env/clusters/:clusterName/namespaces/:namespaceName/commits?page=:page'
+        }
+    });
+    return {
+        find_commits: function (appId, env, clusterName, namespaceName, page) {
+            var d = $q.defer();
+            commit_resource.find_commits({
+                                             appId: appId,
+                                             env: env,
+                                             clusterName: clusterName,
+                                             namespaceName: namespaceName,
+                                             page: page
+                                         },
+                                         function (result) {
+                                             d.resolve(result);
+                                         }, function (result) {
+                    d.reject(result);
+                });
+            return d.promise;
+        }
+    }
+}]);

+ 16 - 31
apollo-portal/src/main/resources/static/styles/common-style.css

@@ -174,7 +174,6 @@ table th {
     border-bottom: 1px solid #ddd;
 }
 
-
 .tocify-header {
     font-size: 14px;
 }
@@ -206,12 +205,12 @@ table th {
     border: 0px;
 }
 
-.config-item-container .second-panel-heading .ns_btn{
+.config-item-container .second-panel-heading .ns_btn {
     width: 25px;
     height: 25px;
 }
 
-.config-item-container .second-panel-heading .nav-tabs .node_active{
+.config-item-container .second-panel-heading .nav-tabs .node_active {
     border-bottom: 3px #1b6d85 solid;
 
 }
@@ -221,26 +220,26 @@ table th {
     overflow: scroll;
 }
 
-.config-item-container .panel-heading button img{
+.config-item-container .panel-heading button img {
     width: 12px;
     height: 12px;
 }
 
-.config-item-container .panel-heading a img{
+.config-item-container .panel-heading a img {
     width: 12px;
     height: 12px;
 }
 
-.config-item-container .panel-heading li img{
+.config-item-container .panel-heading li img {
     width: 12px;
     height: 12px;
 }
 
-.config-item-container .second-panel-heading{
+.config-item-container .second-panel-heading {
     height: 45px;
 }
 
-.config-item-container .second-panel-heading a{
+.config-item-container .second-panel-heading a {
     height: 35px;
     color: #555;
     font-size: 13px;
@@ -248,11 +247,11 @@ table th {
 
 }
 
-.second-panel-heading .nav-tabs{
+.second-panel-heading .nav-tabs {
     border-bottom: 0px;
 }
 
-.namespace-view-table td img{
+.namespace-view-table td img {
     cursor: pointer;
     width: 23px;
     height: 23px;
@@ -272,27 +271,8 @@ table th {
 }
 
 .history-view {
-    padding: 50px 20px;
-
-}
-
-.history-view .commit {
-    padding: 5px 15px;
-    border: 1px solid #ddd;
-}
-
-.history-view img {
-    position: absolute;
-    left: -28px;
-}
+    padding: 10px 20px;
 
-.history-view .media .row {
-    padding-left: 35px;
-}
-
-.history-view .list {
-    position: relative;
-    border-left: 3px solid #ddd;
 }
 
 .line {
@@ -380,7 +360,12 @@ table th {
     padding: 20px 15px
 }
 
-.user-container .user-info{
+.user-container .user-info {
     margin-left: 5px;
 }
 
+.change-type-mark {
+    width: 5px;
+    height: 5px;
+}
+