Browse Source

project group all

xinpeng.fu 2 years ago
parent
commit
96586e5fe8
21 changed files with 1047 additions and 35 deletions
  1. 60 0
      dbsyncer-biz/src/main/java/org/dbsyncer/biz/ProjectGroupService.java
  2. 78 0
      dbsyncer-biz/src/main/java/org/dbsyncer/biz/checker/impl/projectgroup/ProjectGroupChecker.java
  3. 115 0
      dbsyncer-biz/src/main/java/org/dbsyncer/biz/impl/ProjectGroupServiceImpl.java
  4. 12 1
      dbsyncer-manager/src/main/java/org/dbsyncer/manager/Manager.java
  5. 30 1
      dbsyncer-manager/src/main/java/org/dbsyncer/manager/ManagerFactory.java
  6. 5 5
      dbsyncer-manager/src/main/java/org/dbsyncer/manager/config/PreloadCallBack.java
  7. 10 0
      dbsyncer-manager/src/main/java/org/dbsyncer/manager/enums/HandlerEnum.java
  8. 3 1
      dbsyncer-manager/src/main/java/org/dbsyncer/manager/template/impl/PreloadTemplate.java
  9. 70 0
      dbsyncer-parser/src/main/java/org/dbsyncer/parser/model/ProjectGroup.java
  10. 2 2
      dbsyncer-storage/src/main/java/org/dbsyncer/storage/constant/ConfigConstant.java
  11. 26 4
      dbsyncer-web/src/main/java/org/dbsyncer/web/controller/index/IndexController.java
  12. 177 0
      dbsyncer-web/src/main/java/org/dbsyncer/web/controller/index/ProjectGroupController.java
  13. 77 0
      dbsyncer-web/src/main/resources/public/group/addOrEdit.html
  14. 2 1
      dbsyncer-web/src/main/resources/public/index.html
  15. 41 2
      dbsyncer-web/src/main/resources/public/index/index.html
  16. 11 9
      dbsyncer-web/src/main/resources/static/css/index/index.css
  17. 55 4
      dbsyncer-web/src/main/resources/static/js/common.js
  18. 59 0
      dbsyncer-web/src/main/resources/static/js/group/addOrEdit.js
  19. 93 1
      dbsyncer-web/src/main/resources/static/js/index/index.js
  20. 117 0
      dbsyncer-web/src/main/resources/static/plugins/js/jquery/jquery.cookie.js
  21. 4 4
      dbsyncer-web/src/main/resources/static/plugins/js/loading-plus/loading-plus.js

+ 60 - 0
dbsyncer-biz/src/main/java/org/dbsyncer/biz/ProjectGroupService.java

@@ -0,0 +1,60 @@
+package org.dbsyncer.biz;
+
+import org.dbsyncer.parser.model.ProjectGroup;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 项目组
+ *
+ * @author xinpeng.Fu
+ * @version 1.0.0
+ * @date 2022/6/9 17:09
+ **/
+public interface ProjectGroupService {
+
+    /**
+     * 新增项目组
+     *
+     * @param params
+     */
+    String add(Map<String, String> params);
+
+    /**
+     * 修改项目组
+     *
+     * @param params
+     */
+    String edit(Map<String, String> params);
+
+    /**
+     * 删除项目组
+     *
+     * @param id
+     */
+    String remove(String id);
+
+    /**
+     * 获取项目组
+     *
+     * @param id
+     * @return
+     */
+    ProjectGroup getProjectGroup(String id);
+
+    /**
+     * 获取所有项目组
+     *
+     * @return
+     */
+    List<ProjectGroup> getProjectGroupAll();
+
+    /**
+     * 获取项目组详细
+     *
+     * @return
+     */
+    ProjectGroup getProjectGroupDetail(String projectGroupId);
+
+}

+ 78 - 0
dbsyncer-biz/src/main/java/org/dbsyncer/biz/checker/impl/projectgroup/ProjectGroupChecker.java

@@ -0,0 +1,78 @@
+package org.dbsyncer.biz.checker.impl.projectgroup;
+
+import org.dbsyncer.biz.checker.AbstractChecker;
+import org.dbsyncer.common.util.StringUtil;
+import org.dbsyncer.parser.model.ConfigModel;
+import org.dbsyncer.parser.model.ProjectGroup;
+import org.dbsyncer.storage.constant.ConfigConstant;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+
+import static org.dbsyncer.storage.constant.ConfigConstant.CONFIG_MODEL_ID;
+import static org.dbsyncer.storage.constant.ConfigConstant.PROJECT_GROUP;
+
+/**
+ * @author xinpeng.Fu
+ * @version 1.0.0
+ * @date 2022/6/9 17:09
+ **/
+@Component
+public class ProjectGroupChecker extends AbstractChecker {
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+
+    /**
+     * 新增配置
+     *
+     * @param params
+     * @return
+     */
+    @Override
+    public ConfigModel checkAddConfigModel(Map<String, String> params) {
+        String mappingIds = params.get("mappingIds");
+        String connectorIds = params.get("connectorIds");
+        String name = params.get(ConfigConstant.CONFIG_MODEL_NAME);
+        ProjectGroup projectGroup = new ProjectGroup();
+        projectGroup.setMappingIds(StringUtil.isBlank(mappingIds) ? Collections.emptyList() : Arrays.asList(mappingIds.split(",")));
+        projectGroup.setConnectorIds(StringUtil.isBlank(connectorIds) ? Collections.emptyList() : Arrays.asList(connectorIds.split(",")));
+        projectGroup.setType(PROJECT_GROUP);
+        projectGroup.setName(name);
+
+
+        // 修改基本配置
+        this.modifyConfigModel(projectGroup, params);
+
+        return projectGroup;
+    }
+
+    /**
+     * 修改配置
+     *
+     * @param params
+     * @return
+     */
+    @Override
+    public ConfigModel checkEditConfigModel(Map<String, String> params) {
+        String id = params.get(CONFIG_MODEL_ID);
+        String mappingIds = params.get("mappingIds");
+        String connectorIds = params.get("connectorIds");
+        String name = params.get(ConfigConstant.CONFIG_MODEL_NAME);
+        ProjectGroup projectGroup = new ProjectGroup();
+        projectGroup.setMappingIds(StringUtil.isBlank(mappingIds) ? Collections.emptyList() : Arrays.asList(mappingIds.split(",")));
+        projectGroup.setConnectorIds(StringUtil.isBlank(connectorIds) ? Collections.emptyList() : Arrays.asList(connectorIds.split(",")));
+        projectGroup.setType(PROJECT_GROUP);
+        projectGroup.setName(name);
+        projectGroup.setId(id);
+
+        // 修改基本配置
+        this.modifyConfigModel(projectGroup, params);
+
+        return projectGroup;
+    }
+}

+ 115 - 0
dbsyncer-biz/src/main/java/org/dbsyncer/biz/impl/ProjectGroupServiceImpl.java

@@ -0,0 +1,115 @@
+package org.dbsyncer.biz.impl;
+
+import org.dbsyncer.biz.ConnectorService;
+import org.dbsyncer.biz.MappingService;
+import org.dbsyncer.biz.ProjectGroupService;
+import org.dbsyncer.biz.checker.Checker;
+import org.dbsyncer.biz.vo.MappingVo;
+import org.dbsyncer.common.util.StringUtil;
+import org.dbsyncer.manager.Manager;
+import org.dbsyncer.parser.logger.LogType;
+import org.dbsyncer.parser.model.ConfigModel;
+import org.dbsyncer.parser.model.Connector;
+import org.dbsyncer.parser.model.ProjectGroup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 项目组
+ *
+ * @author xinpeng.Fu
+ * @version 1.0.0
+ * @date 2022/6/9 17:09
+ **/
+@Service
+public class ProjectGroupServiceImpl extends BaseServiceImpl implements ProjectGroupService {
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Autowired
+    private ConnectorService connectorService;
+
+    @Autowired
+    private MappingService mappingService;
+
+    @Autowired
+    private Manager manager;
+
+    @Autowired
+    private Checker projectGroupChecker;
+
+    @Override
+    public String add(Map<String, String> params) {
+        ConfigModel model = projectGroupChecker.checkAddConfigModel(params);
+        log(LogType.ConnectorLog.INSERT, model);
+
+        return manager.addProjectGroup(model);
+    }
+
+    @Override
+    public String edit(Map<String, String> params) {
+        ConfigModel model = projectGroupChecker.checkEditConfigModel(params);
+        log(LogType.ConnectorLog.UPDATE, model);
+
+        return manager.editProjectGroup(model);
+    }
+
+    @Override
+    public String remove(String id) {
+        ProjectGroup projectGroup = manager.getProjectGroup(id);
+        log(LogType.ConnectorLog.DELETE, projectGroup);
+        Assert.notNull(projectGroup, "该项目组已被删除");
+        manager.removeProjectGroup(id);
+        return "删除项目组成功!";
+    }
+
+    @Override
+    public ProjectGroup getProjectGroup(String id) {
+        return StringUtil.isNotBlank(id) ? manager.getProjectGroup(id) : null;
+    }
+
+    @Override
+    public List<ProjectGroup> getProjectGroupAll() {
+        List<ProjectGroup> list = manager.getProjectGroupAll();
+        return list;
+    }
+
+    @Override
+    public ProjectGroup getProjectGroupDetail(String projectGroupId) {
+        ProjectGroup projectGroup = this.getProjectGroup(projectGroupId);
+        if(null == projectGroup){
+            return null;
+        }
+        // 过滤出已经选择的
+        List<String> connectorIds = projectGroup.getConnectorIds();
+        if(CollectionUtils.isEmpty(connectorIds)){
+            projectGroup.setConnectors(Collections.emptyList());
+        }else{
+            Set<String> connectorIdSet = new HashSet<>(connectorIds);
+            List<Connector> connectors = connectorService.getConnectorAll();
+            projectGroup.setConnectors(connectors.stream()
+                    .filter((connector -> connectorIdSet.contains(connector.getId())))
+                    .collect(Collectors.toList())
+            );
+        }
+        List<String> mappingIds = projectGroup.getMappingIds();
+        if(CollectionUtils.isEmpty(mappingIds)){
+            projectGroup.setMappings(Collections.emptyList());
+        }else{
+            Set<String> mappingIdSet = new HashSet<>(mappingIds);
+            List<MappingVo> mappings = mappingService.getMappingAll();
+            projectGroup.setMappings(mappings.stream()
+                    .filter((mapping -> mappingIdSet.contains(mapping.getId())))
+                    .collect(Collectors.toList())
+            );
+        }
+        return projectGroup;
+    }
+}

+ 12 - 1
dbsyncer-manager/src/main/java/org/dbsyncer/manager/Manager.java

@@ -27,6 +27,17 @@ import java.util.Map;
  */
 public interface Manager extends Executor {
 
+    // project group
+    String addProjectGroup(ConfigModel model);
+
+    String editProjectGroup(ConfigModel model);
+
+    ProjectGroup getProjectGroup(String id);
+
+    void removeProjectGroup(String id);
+
+    List<ProjectGroup> getProjectGroupAll();
+
     // Connector
     ConnectorMapper connect(ConnectorConfig config);
 
@@ -132,4 +143,4 @@ public interface Manager extends Executor {
     String getLibraryPath();
 
     void loadPlugins();
-}
+}

+ 30 - 1
dbsyncer-manager/src/main/java/org/dbsyncer/manager/ManagerFactory.java

@@ -66,6 +66,35 @@ public class ManagerFactory implements Manager, ApplicationListener<ClosedEvent>
     @Autowired
     private Map<String, Puller> map;
 
+    @Override
+    public String addProjectGroup(ConfigModel model) {
+        return operationTemplate.execute(new OperationConfig(model, HandlerEnum.OPR_ADD.getHandler()));
+    }
+
+    @Override
+    public String editProjectGroup(ConfigModel model) {
+        return operationTemplate.execute(new OperationConfig(model, HandlerEnum.OPR_EDIT.getHandler()));
+    }
+
+    @Override
+    public ProjectGroup getProjectGroup(String id) {
+        return operationTemplate.queryObject(ProjectGroup.class, id);
+    }
+
+    @Override
+    public void removeProjectGroup(String id) {
+        operationTemplate.remove(new OperationConfig(id));
+    }
+
+    @Override
+    public List<ProjectGroup> getProjectGroupAll() {
+        ProjectGroup projectGroup = new ProjectGroup();
+        projectGroup.setType(ConfigConstant.PROJECT_GROUP);
+        QueryConfig<ProjectGroup> queryConfig = new QueryConfig<>(projectGroup);
+        List<ProjectGroup> groups = operationTemplate.queryAll(queryConfig);
+        return groups;
+    }
+
     @Override
     public ConnectorMapper connect(ConnectorConfig config) {
         return parser.connect(config);
@@ -383,4 +412,4 @@ public class ManagerFactory implements Manager, ApplicationListener<ClosedEvent>
         return puller;
     }
 
-}
+}

+ 5 - 5
dbsyncer-manager/src/main/java/org/dbsyncer/manager/config/PreloadCallBack.java

@@ -2,10 +2,7 @@ package org.dbsyncer.manager.config;
 
 import org.dbsyncer.manager.template.Callback;
 import org.dbsyncer.parser.Parser;
-import org.dbsyncer.parser.model.Config;
-import org.dbsyncer.parser.model.Mapping;
-import org.dbsyncer.parser.model.Meta;
-import org.dbsyncer.parser.model.TableGroup;
+import org.dbsyncer.parser.model.*;
 
 public class PreloadCallBack implements Callback {
 
@@ -38,4 +35,7 @@ public class PreloadCallBack implements Callback {
         return parser.parseObject(json, Config.class);
     }
 
-}
+    public Object parseProjectGroup() {
+        return parser.parseObject(json, ProjectGroup.class);
+    }
+}

+ 10 - 0
dbsyncer-manager/src/main/java/org/dbsyncer/manager/enums/HandlerEnum.java

@@ -75,6 +75,16 @@ public enum HandlerEnum {
         protected Object preload(PreloadCallBack preloadCallBack) {
             return preloadCallBack.parseConfig();
         }
+    }),
+
+    /**
+     * 预加载ProjectGroup
+     */
+    PRELOAD_PROJECT_GROUP(new AbstractPreloadHandler(){
+        @Override
+        protected Object preload(PreloadCallBack preloadCallBack) {
+            return preloadCallBack.parseProjectGroup();
+        }
     });
 
     private Handler handler;

+ 3 - 1
dbsyncer-manager/src/main/java/org/dbsyncer/manager/template/impl/PreloadTemplate.java

@@ -98,6 +98,8 @@ public final class PreloadTemplate extends AbstractTemplate implements Applicati
         execute(new PreloadConfig(ConfigConstant.META, HandlerEnum.PRELOAD_META));
         // Load configs
         execute(new PreloadConfig(ConfigConstant.CONFIG, HandlerEnum.PRELOAD_CONFIG));
+        // Load projectGroups
+        execute(new PreloadConfig(ConfigConstant.PROJECT_GROUP, HandlerEnum.PRELOAD_PROJECT_GROUP));
 
         // Load plugins
         manager.loadPlugins();
@@ -123,4 +125,4 @@ public final class PreloadTemplate extends AbstractTemplate implements Applicati
         }
     }
 
-}
+}

+ 70 - 0
dbsyncer-parser/src/main/java/org/dbsyncer/parser/model/ProjectGroup.java

@@ -0,0 +1,70 @@
+package org.dbsyncer.parser.model;
+
+import org.dbsyncer.common.util.CollectionUtils;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author xinpeng.Fu
+ * @version 1.0.0
+ * @date 2022/6/9 17:09
+ **/
+public class ProjectGroup extends ConfigModel {
+
+    /**
+     * 链接列表
+     */
+    private List<String> connectorIds;
+
+
+    /**
+     * 映射id列表
+     */
+    private List<String> mappingIds;
+
+    private List<Connector> connectors;
+
+    private List<Mapping> mappings;
+
+    public List<String> getConnectorIds() {
+        return connectorIds;
+    }
+
+    public String getConnectorIdStr() {
+        String connectorStr = CollectionUtils.isEmpty(connectorIds) ? "" : connectorIds.stream().collect(Collectors.joining(","));
+        return connectorStr;
+    }
+
+    public void setConnectorIds(List<String> connectorIds) {
+        this.connectorIds = connectorIds;
+    }
+
+    public List<String> getMappingIds() {
+        return mappingIds;
+    }
+
+    public String getMappingIdStr() {
+        String mappingStr = CollectionUtils.isEmpty(mappingIds) ? "" : mappingIds.stream().collect(Collectors.joining(","));
+        return mappingStr;
+    }
+    public void setMappingIds(List<String> mappingIds) {
+        this.mappingIds = mappingIds;
+    }
+
+    public List<Connector> getConnectors() {
+        return connectors;
+    }
+
+    public void setConnectors(List<Connector> connectors) {
+        this.connectors = connectors;
+    }
+
+    public List<Mapping> getMappings() {
+        return mappings;
+    }
+
+    public void setMappings(List<Mapping> mappings) {
+        this.mappings = mappings;
+    }
+}

+ 2 - 2
dbsyncer-storage/src/main/java/org/dbsyncer/storage/constant/ConfigConstant.java

@@ -16,7 +16,7 @@ public class ConfigConstant {
     public static final String CONFIG_MODEL_CREATE_TIME = "createTime";
     public static final String CONFIG_MODEL_UPDATE_TIME = "updateTime";
     public static final String CONFIG_MODEL_JSON = "json";
-
+    public static final String PROJECT_GROUP = "projectGroup";
     /**
      * 配置类型
      */
@@ -33,4 +33,4 @@ public class ConfigConstant {
     public static final String DATA_EVENT = "event";
     public static final String DATA_ERROR = "error";
 
-}
+}

+ 26 - 4
dbsyncer-web/src/main/java/org/dbsyncer/web/controller/index/IndexController.java

@@ -1,10 +1,15 @@
 package org.dbsyncer.web.controller.index;
 
+import org.apache.commons.lang3.StringUtils;
 import org.dbsyncer.biz.ConnectorService;
 import org.dbsyncer.biz.MappingService;
+import org.dbsyncer.biz.ProjectGroupService;
+import org.dbsyncer.biz.vo.MappingVo;
 import org.dbsyncer.biz.vo.RestResult;
 import org.dbsyncer.biz.vo.VersionVo;
 import org.dbsyncer.common.config.AppConfig;
+import org.dbsyncer.parser.model.Connector;
+import org.dbsyncer.parser.model.ProjectGroup;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 import org.springframework.ui.ModelMap;
@@ -14,6 +19,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
 
 import javax.servlet.http.HttpServletRequest;
 import java.io.UnsupportedEncodingException;
+import java.util.List;
 
 @Controller
 @RequestMapping("/index")
@@ -27,11 +33,26 @@ public class IndexController {
 
     @Autowired
     private AppConfig appConfig;
+    @Autowired
+    private ProjectGroupService projectGroupService;
+
 
     @GetMapping("")
-    public String index(HttpServletRequest request, ModelMap model) {
-        model.put("connectors", connectorService.getConnectorAll());
-        model.put("mappings", mappingService.getMappingAll());
+    public String index(HttpServletRequest request, ModelMap model, String projectGroupId) {
+        ProjectGroup projectGroup = projectGroupService.getProjectGroup(projectGroupId);
+        if (StringUtils.isBlank(projectGroupId) || null == projectGroup) {
+            List<Connector> connectors = connectorService.getConnectorAll();
+            List<MappingVo> mappings = mappingService.getMappingAll();
+            model.put("connectors", connectors);
+            model.put("mappings", mappings);
+            model.put("selectedGroup", "");
+        } else {
+            ProjectGroup projectGroupDetail = projectGroupService.getProjectGroupDetail(projectGroupId);
+            model.put("connectors", projectGroupDetail.getConnectors());
+            model.put("mappings", projectGroupDetail.getMappings());
+            model.put("selectedGroup", projectGroupId);
+        }
+        model.put("groups", projectGroupService.getProjectGroupAll());
         return "index/index.html";
     }
 
@@ -41,4 +62,5 @@ public class IndexController {
         return RestResult.restSuccess(new VersionVo(appConfig.getName(), appConfig.getCopyright()));
     }
 
-}
+}
+

+ 177 - 0
dbsyncer-web/src/main/java/org/dbsyncer/web/controller/index/ProjectGroupController.java

@@ -0,0 +1,177 @@
+package org.dbsyncer.web.controller.index;
+
+import org.dbsyncer.biz.ConnectorService;
+import org.dbsyncer.biz.MappingService;
+import org.dbsyncer.biz.ProjectGroupService;
+import org.dbsyncer.biz.vo.RestResult;
+import org.dbsyncer.parser.model.ProjectGroup;
+import org.dbsyncer.web.controller.BaseController;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.util.Assert;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Map;
+/**
+ * 项目组
+ *
+ * @author xinpeng.Fu
+ * @date 2022/6/23 14:59
+ **/
+@Controller
+@RequestMapping("/projectGroup")
+public class ProjectGroupController extends BaseController {
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+    @Autowired
+    private ConnectorService connectorService;
+    @Autowired
+    private MappingService mappingService;
+    @Autowired
+    private ProjectGroupService projectGroupService;
+
+    @GetMapping("/page/add")
+    public String pageAdd(HttpServletRequest request, ModelMap model) {
+        model.put("connectors", connectorService.getConnectorAll());
+        model.put("mappings", mappingService.getMappingAll());
+        return "group/addOrEdit";
+    }
+
+    @GetMapping("/page/edit")
+    public String pageEdit(HttpServletRequest request, ModelMap model, String id) {
+        ProjectGroup projectGroup = projectGroupService.getProjectGroup(id);
+        model.put("projectGroup", projectGroup);
+        model.put("connectors", connectorService.getConnectorAll());
+        model.put("mappings", mappingService.getMappingAll());
+        model.put("selectedConnectors", projectGroup.getConnectorIdStr());
+        model.put("selectedMappings", projectGroup.getMappingIdStr());
+
+        return "group/addOrEdit";
+    }
+
+    /**
+     * 参数:
+     * name(必)
+     * mappingIds
+     * connectorIds
+     *
+     * @param request
+     * @return org.dbsyncer.biz.vo.RestResult
+     * @author xinpeng.Fu
+     * @date 2022/6/15 16:10
+     **/
+    @PostMapping("/add")
+    @ResponseBody
+    public RestResult add(HttpServletRequest request) {
+        try {
+            Map<String, String> params = getParams(request);
+            return RestResult.restSuccess(projectGroupService.add(params));
+        } catch (Exception e) {
+            logger.error("add project group error:", e);
+            return RestResult.restFail(e.getMessage());
+        }
+    }
+
+    /**
+     * 参数:
+     * id(必)
+     * name(必)
+     * mappingIds
+     * connectorIds
+     *
+     *
+     * @param request
+     * @return org.dbsyncer.biz.vo.RestResult
+     * @author xinpeng.Fu
+     * @date 2022/6/15 16:10
+     **/
+    @PostMapping("/edit")
+    @ResponseBody
+    public RestResult edit(HttpServletRequest request) {
+        try {
+            Map<String, String> params = getParams(request);
+            return RestResult.restSuccess(projectGroupService.edit(params));
+        } catch (Exception e) {
+            logger.error("edit project group error:", e);
+            return RestResult.restFail(e.getMessage());
+        }
+    }
+
+    /**
+     * 参数:
+     * id(必)
+     *
+     * @param request
+     * @return org.dbsyncer.biz.vo.RestResult
+     * @author xinpeng.Fu
+     * @date 2022/6/15 16:10
+     **/
+    @PostMapping("/remove")
+    @ResponseBody
+    public RestResult remove(HttpServletRequest request, @RequestParam(value = "id") String id) {
+        try {
+            return RestResult.restSuccess(projectGroupService.remove(id));
+        } catch (Exception e) {
+            logger.error(e.getLocalizedMessage(), e.getClass());
+            return RestResult.restFail(e.getMessage());
+        }
+    }
+
+    /**
+     * 参数:
+     * id(必)
+     *
+     * @param request
+     * @return org.dbsyncer.biz.vo.RestResult
+     * @author xinpeng.Fu
+     * @date 2022/6/15 16:10
+     **/
+    @GetMapping("/get")
+    @ResponseBody
+    public RestResult get(HttpServletRequest request, @RequestParam(value = "id") String id) {
+        try {
+            ProjectGroup projectGroup = projectGroupService.getProjectGroup(id);
+            Assert.notNull(projectGroup, "该项目组已被删除");
+            return RestResult.restSuccess(projectGroup);
+        } catch (Exception e) {
+            logger.error(e.getLocalizedMessage(), e.getClass());
+            return RestResult.restFail(e.getMessage());
+        }
+    }
+
+    /**
+     * 参数:
+     * id(必)
+     *
+     * @param request
+     * @return org.dbsyncer.biz.vo.RestResult
+     * @author xinpeng.Fu
+     * @date 2022/6/15 16:10
+     **/
+    @GetMapping("/getDetail")
+    @ResponseBody
+    public RestResult getDetail(HttpServletRequest request, @RequestParam(value = "id") String id) {
+        try {
+            return RestResult.restSuccess(projectGroupService.getProjectGroupDetail(id));
+        } catch (Exception e) {
+            logger.error("getDetail error:", e);
+            return RestResult.restFail(e.getMessage());
+        }
+    }
+
+    @GetMapping("/getAll")
+    @ResponseBody
+    public RestResult getAll(HttpServletRequest request) {
+        try {
+            return RestResult.restSuccess(projectGroupService.getProjectGroupAll());
+        } catch (Exception e) {
+            logger.error(e.getLocalizedMessage(), e.getClass());
+            return RestResult.restFail(e.getMessage());
+        }
+    }
+
+}

+ 77 - 0
dbsyncer-web/src/main/resources/public/group/addOrEdit.html

@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
+
+<div class="container-fluid">
+  <div class="container">
+    <form id="groupAddForm" class="form-horizontal" role="form" method="post">
+      <!-- 标题 -->
+      <div class="row text-center">
+        <div class="page-header">
+          <h3>添加项目组</h3>
+        </div>
+      </div>
+
+      <!-- 操作 -->
+      <div class="form-group">
+        <div class="col-md-10"></div>
+        <div class="col-md-2 text-right">
+          <button id="groupSubmitBtn" type="button" class="btn btn-primary">
+            <span class="fa fa-save"></span>保存
+          </button>
+          <button id="groupBackBtn" type="button" class="btn btn-default">
+            <span class="fa fa-reply"></span>返回
+          </button>
+        </div>
+      </div>
+
+      <!-- 配置 -->
+      <div class="row">
+        <div class="col-md-12">
+          <div class="panel panel-info">
+            <div class="panel-heading">
+              <h3 class="panel-title">项目组配置</h3>
+            </div>
+
+            <div class="panel-body">
+              <!-- 名称 -->
+              <div class="form-group">
+                <label class="col-sm-2 control-label">名称 <strong
+                        class="driverVerifcateRequired">*</strong></label>
+                <div class="col-sm-10">
+                  <input type="hidden" name="id" id="id" th:value="${projectGroup?.id}">
+                  <input class="form-control" name="name" type="text" maxlength="50" dbsyncer-valid="require" placeholder="名称" th:value="${projectGroup?.name}"/>
+                </div>
+              </div>
+              <!-- 选择连接 -->
+              <div class="form-group">
+                <label class="col-sm-2 control-label">连接</label>
+                <div class="col-sm-10">
+                  <select id="connectorIds" name="connectorIds" class="form-control select-control-table" multiple="multiple">
+                    <option th:each="t,s:${connectors}" th:value="${t?.id}" th:text="${t?.name}" />
+                  </select>
+                  <input id="selectedConnectors" type="hidden" th:value="${selectedConnectors}"/>
+                </div>
+              </div>
+              <!-- 选择驱动 -->
+              <div class="form-group">
+                <label class="col-sm-2 control-label">驱动</label>
+                <div class="col-sm-10">
+                  <select id="mappingIds" name="mappingIds" class="form-control select-control-table" multiple="multiple">
+                    <option th:each="t,s:${mappings}" th:value="${t?.id}" th:text="${t?.name}" />
+                  </select>
+                  <input id="selectedMappings" type="hidden" th:value="${selectedMappings}"/>
+                </div>
+              </div>
+
+            </div>
+          </div>
+        </div>
+      </div>
+
+    </form>
+  </div>
+</div>
+
+<script th:src="@{/js/group/addOrEdit.js}"></script>
+</html>

+ 2 - 1
dbsyncer-web/src/main/resources/public/index.html

@@ -40,6 +40,7 @@
 <script th:src="@{/plugins/js/bootstrap/html5shiv.min.js}"></script>
 <!-- 上述2个js文件解决IE8以上bootstrap的兼容性问题 -->
 <script th:src="@{/plugins/js/jquery/jquery-1.11.3.min.js}"></script>
+<script th:src="@{/plugins/js/jquery/jquery.cookie.js}"></script>
 <script th:src="@{/plugins/js/bootstrap/bootstrap.min.js}"></script>
 <script th:src="@{/plugins/js/bootstrap-dialog/bootstrap-dialog.min.js}"></script>
 <script th:src="@{/plugins/js/bootstrap-growl/jquery.bootstrap-growl.min.js}"></script>
@@ -56,4 +57,4 @@
 <script th:src="@{/plugins/js/echarts/echarts.min.js}"></script>
 <script th:src="@{/js/common.js}"></script>
 <script th:src="@{/js/index.js}"></script>
-</html>
+</html>

+ 41 - 2
dbsyncer-web/src/main/resources/public/index/index.html

@@ -6,7 +6,46 @@
 <div class="container-fluid">
     <div class="row">
         <form class="form-horizontal" role="form" method="post">
+            <!-- 项目组管理 -->
+            <div class="col-md-12">
+                <!-- 项目组开始位置 -->
+                <div class="form-group">
+                    <div class="col-md-12">
+                        <button type="button" class="btn btn-primary" id="addGroupBtn">
+                            <span class="fa fa-plus"></span>添加项目组([[${groups?.size()} ?: 0]])
+                        </button>
+                    </div>
+                </div>
+                <!-- 显示项目组 -->
+                <div class="row" th:if="${groups?.size() gt 0}">
+                    <div class="col-md-12">
+                        <div class="panel panel-default">
+                            <div class="panel-body">
+                                <div class="row">
+                                    <div class="col-md-4">
+                                        <select id="group" name="group" class="form-control select-control">
+                                            <option value="" th:text="全部" selected/>
+                                            <option th:each="t,s:${groups}" th:value="${t?.id}" th:text="${t?.name}" />
+                                        </select>
+                                         <input id="selectedGroup" type="hidden" th:value="${selectedGroup}"/>
+                                    </div>
+                                    <div class="col-md-4">
+                                        <button type="button" class="btn btn-primary" id="editGroupBtn">
+                                            <span class="fa fa-pencil"></span>编辑项目组
+                                        </button>
+                                        <button type="button" class="btn btn-primary" id="delGroupBtn">
+                                            <span class="fa fa-times"></span>删除项目组
+                                        </button>
+                                    </div>
+                                    <div class="col-md-4">
 
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
             <!-- 连接管理 -->
             <div class="col-md-12">
                 <!-- 连接开始位置 -->
@@ -72,7 +111,7 @@
                                 <div class="row mappingList">
                                     <!-- 驱动__开始 -->
                                     <div class="col-md-4" th:each="m,state : ${mappings}">
-                                        <div th:id="${m?.id}" class="jumbotron dbsyncer_block">
+                                        <div th:id="${m?.id}" th:class="${m?.meta?.model eq '全量'} ? 'jumbotron dbsyncer_block dbsyncer_block_full' : 'jumbotron dbsyncer_block dbsyncer_block_increment'">
                                             <!--驱动标题信息 -->
                                             <div class="row text-center" th:text="${m?.name}" th:title="${m?.name}"></div>
 
@@ -183,4 +222,4 @@
 </div>
 
 <script th:src="@{/js/index/index.js}"></script>
-</html>
+</html>

+ 11 - 9
dbsyncer-web/src/main/resources/static/css/index/index.css

@@ -1,13 +1,7 @@
 @charset "UTF-8";
-.connectorList img {
-    width: 65px;
-    height: 65px;
-    border-radius: 20%;
-}
-
-.connectorList .well-sign-red {
-    color: #ff0000;
-}
+.groupBtn{display: block; margin-bottom: 10px;}
+.connectorList img{width: 65px;height: 65px; border-radius: 20%;}
+.connectorList .well-sign-red{color: #ff0000;}
 .connectorList .well-sign-operation{color: #999999; border-radius: 50%; }
 .connectorList .well-sign-operation:hover {color: #333333;}
 .connectorList .dropdown {position: absolute; right:16px; top: 0px;}
@@ -29,3 +23,11 @@
 .mappingList .dropdown {position: absolute; right:0px; top: 0px;}
 .mappingList .dropdown-menu {right: 12px;left: auto; top: 28px;}
 .mappingList .dropdown-menu i{font-size: 20px;}
+
+.dbsyncer_block_increment {
+  background: -webkit-linear-gradient(left, #ffffff, #dffbdc);
+}
+
+.dbsyncer_block_full {
+  background: -webkit-linear-gradient(left, #ffffff, #dcf5fb);
+}

+ 55 - 4
dbsyncer-web/src/main/resources/static/js/common.js

@@ -13,7 +13,7 @@ var timer;
 function bootGrowl(data, type) {
     $.bootstrapGrowl(data, { // data为提示信息
         type: type == undefined ? 'success' : type,// type指提示类型
-        delay: 1000,// 提示框显示时间
+        delay: 3000,// 提示框显示时间
         allow_dismiss: true // 显示取消提示框
     });
 }
@@ -21,7 +21,8 @@ function bootGrowl(data, type) {
 // 跳转主页
 function backIndexPage() {
     // 加载页面
-    doLoader("/index?refresh=" + new Date().getTime());
+    var groupID = isBlank($.cookie("groupID")) ? "" : $.cookie("groupID");
+    doLoader("/index?projectGroupId="+ groupID + "&refresh=" + new Date().getTime());
 }
 
 // 美化SQL
@@ -84,7 +85,12 @@ $.fn.serializeJson = function () {
 // 全局加载页面
 function doLoader(url){
     // 加载页面
-    $initContainer.load($basePath + url);
+    $initContainer.load($basePath + url, function (response,status,xhr) {
+        if (status != 'success') {
+            bootGrowl(response);
+        }
+    });
+    $.loadingT(false);
 }
 
 // 异常请求
@@ -132,4 +138,49 @@ function doGetter(url, params, action, loading) {
 // 全局Ajax get, 不显示加载动画
 function doGetWithoutLoading(url, params, action) {
     doGetter(url, params, action, false);
-}
+}
+
+/**
+ * 判断字符串是否为空串
+ * @eg undefined true
+ * @eg null true
+ * @eg '' true
+ * @eg ' ' true
+ * @eg '1' false
+ *
+ */
+function isBlank(str) {
+    return str === undefined || str === null || str === false || str.length === 0 || str.replaceAll(' ', '').length === 0;
+}
+
+/**
+ * 移除数组中的空元素
+ *
+ * @param args 原数组
+ *
+ * @return Array
+ *
+*/
+function filterEmptyElements(args) {
+    if ($.isEmptyObject(args)) {
+        return [];
+    }
+    args = args.filter(e => !isBlank(e));
+    return args;
+}
+
+/**
+ * 按照指定分隔符切分字符串
+ *
+ * @param str 带切分字符
+ * @param delimiter 分隔符
+ *
+ * @return Array
+ *
+ */
+function splitStrByDelimiter(str, delimiter) {
+    if (isBlank(str)) {
+        return;
+    }
+    return str.split(delimiter);
+}

+ 59 - 0
dbsyncer-web/src/main/resources/static/js/group/addOrEdit.js

@@ -0,0 +1,59 @@
+/**
+ *
+ * Created by zhichao.qin on 2022/6/15
+ */
+function submit(data) {
+    if (data["id"]) {
+        doPoster("/projectGroup/edit", data, function (data) {
+            if (data.success == true) {
+                bootGrowl("修改项目组成功!", "success");
+                backIndexPage();
+            } else {
+                bootGrowl(data.resultValue, "danger");
+            }
+        });
+    } else {
+        doPoster("/projectGroup/add", data, function (data) {
+            if (data.success == true) {
+                bootGrowl("新增项目组成功!", "success");
+                backIndexPage();
+            } else {
+                bootGrowl(data.resultValue, "danger");
+            }
+        });
+    }
+}
+
+function initSelectByValueOrDefault($select, $selectedValue) {
+
+    $.each($select, function () {
+        var values = splitStrByDelimiter($selectedValue, ",");
+        $(this).selectpicker('val', values);
+    });
+}
+
+$(function () {
+    // 初始化select插件
+    initSelect($(".select-control-table"));
+
+    initSelectByValueOrDefault($("#connectorIds"), $("#selectedConnectors").val());
+    initSelectByValueOrDefault($("#mappingIds"), $("#selectedMappings").val());
+
+    //保存
+    $("#groupSubmitBtn").click(function () {
+        var $form = $("#groupAddForm");
+        if ($form.formValidate() == true) {
+            var data = $form.serializeJson();
+            var connectorIds = data['connectorIds'];
+            var mappingIds = data['mappingIds'];
+            data['connectorIds'] = (connectorIds instanceof Array) ? connectorIds.join(',') : connectorIds;
+            data['mappingIds'] = (mappingIds instanceof Array) ? mappingIds.join(',') : mappingIds;
+            submit(data);
+        }
+    });
+
+    //返回
+    $("#groupBackBtn").click(function () {
+        backIndexPage();
+    });
+})

+ 93 - 1
dbsyncer-web/src/main/resources/static/js/index/index.js

@@ -1,3 +1,80 @@
+// 添加项目组
+function bindAddGroup() {
+    $("#addGroupBtn").click(function () {
+        doLoader("/projectGroup/page/add");
+    });
+}
+// 编辑项目组
+function bindEditGroup() {
+    $("#editGroupBtn").click(function () {
+        var $id = $('#group').val();
+        if (isBlank($id)) {
+            bootGrowl("请选择有效项目组", "danger");
+            return false;
+        }
+        doGetter('/projectGroup/get', {id: $id}, function (data) {
+            if (data.success) {
+                doLoader('/projectGroup/page/edit?id=' + $id);
+            } else {
+                bootGrowl(data.resultValue, 'danger');
+            }
+        });
+    });
+}
+// 删除项目组
+function bindRemoveGroup() {
+    $("#delGroupBtn").click(function () {
+        var $id = $('#group').val();
+        if (isBlank($id)) {
+            bootGrowl("请选择有效项目组", "danger");
+            return false;
+        }
+        BootstrapDialog.show({
+            title: "警告",
+            type: BootstrapDialog.TYPE_DANGER,
+            message: "确认删除?",
+            size: BootstrapDialog.SIZE_NORMAL,
+            buttons: [{
+                label: "确定",
+                action: function (dialog) {
+                    doPoster('/projectGroup/remove',{id: $id}, function (data) {
+                        if (data.success == true) {
+                            // 显示主页
+                            backIndexPage();
+                            bootGrowl(data.resultValue, "success");
+                        } else {
+                            bootGrowl(data.resultValue, "danger");
+                        }
+                        $.cookie("groupID", '');
+                    });
+                    dialog.close();
+                }
+            }, {
+                label: "取消",
+                action: function (dialog) {
+                    dialog.close();
+                }
+            }]
+        });
+    });
+}
+// 项目组筛选
+function bindGroupChange() {
+    var $group = $('#group');
+    // 绑定选择事件
+    $group.on('change, changed.bs.select', function () {
+        var groupID = $(this).val();
+        $.cookie("groupID", groupID);
+        searchByGroup(groupID);
+    });
+}
+
+// 根据项目组查询
+function searchByGroup(groupID) {
+    $.loadingT(true);
+    doLoader("/index?projectGroupId=" + groupID);
+}
+
 // 添加连接
 function bindAddConnector() {
     // 绑定添加连接按钮点击事件
@@ -115,6 +192,21 @@ function doPost(url) {
 }
 
 $(function () {
+    $(".select-control").selectpicker({
+        "style":'dbsyncer_btn-info',
+        "title":"全部",
+        "actionsBox":true,
+        "liveSearch":true,
+        "noneResultsText":"没有找到 {0}",
+        "selectedTextFormat":"count > 10"
+    });
+    // 初始化group
+    $('#group').selectpicker('val', $('#selectedGroup').val());
+
+    bindAddGroup();
+    bindEditGroup();
+    bindRemoveGroup();
+    bindGroupChange();
 
     bindAddConnector();
     bindEditConnector();
@@ -126,4 +218,4 @@ $(function () {
     bindConnectorDropdownMenu();
     bindMappingDropdownMenu();
 
-});
+});

+ 117 - 0
dbsyncer-web/src/main/resources/static/plugins/js/jquery/jquery.cookie.js

@@ -0,0 +1,117 @@
+/*!
+ * jQuery Cookie Plugin v1.4.1
+ * https://github.com/carhartl/jquery-cookie
+ *
+ * Copyright 2013 Klaus Hartl
+ * Released under the MIT license
+ */
+(function (factory) {
+	if (typeof define === 'function' && define.amd) {
+		// AMD
+		define(['jquery'], factory);
+	} else if (typeof exports === 'object') {
+		// CommonJS
+		factory(require('jquery'));
+	} else {
+		// Browser globals
+		factory(jQuery);
+	}
+}(function ($) {
+
+	var pluses = /\+/g;
+
+	function encode(s) {
+		return config.raw ? s : encodeURIComponent(s);
+	}
+
+	function decode(s) {
+		return config.raw ? s : decodeURIComponent(s);
+	}
+
+	function stringifyCookieValue(value) {
+		return encode(config.json ? JSON.stringify(value) : String(value));
+	}
+
+	function parseCookieValue(s) {
+		if (s.indexOf('"') === 0) {
+			// This is a quoted cookie as according to RFC2068, unescape...
+			s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
+		}
+
+		try {
+			// Replace server-side written pluses with spaces.
+			// If we can't decode the cookie, ignore it, it's unusable.
+			// If we can't parse the cookie, ignore it, it's unusable.
+			s = decodeURIComponent(s.replace(pluses, ' '));
+			return config.json ? JSON.parse(s) : s;
+		} catch(e) {}
+	}
+
+	function read(s, converter) {
+		var value = config.raw ? s : parseCookieValue(s);
+		return $.isFunction(converter) ? converter(value) : value;
+	}
+
+	var config = $.cookie = function (key, value, options) {
+
+		// Write
+
+		if (value !== undefined && !$.isFunction(value)) {
+			options = $.extend({}, config.defaults, options);
+
+			if (typeof options.expires === 'number') {
+				var days = options.expires, t = options.expires = new Date();
+				t.setTime(+t + days * 864e+5);
+			}
+
+			return (document.cookie = [
+				encode(key), '=', stringifyCookieValue(value),
+				options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
+				options.path    ? '; path=' + options.path : '',
+				options.domain  ? '; domain=' + options.domain : '',
+				options.secure  ? '; secure' : ''
+			].join(''));
+		}
+
+		// Read
+
+		var result = key ? undefined : {};
+
+		// To prevent the for loop in the first place assign an empty array
+		// in case there are no cookies at all. Also prevents odd result when
+		// calling $.cookie().
+		var cookies = document.cookie ? document.cookie.split('; ') : [];
+
+		for (var i = 0, l = cookies.length; i < l; i++) {
+			var parts = cookies[i].split('=');
+			var name = decode(parts.shift());
+			var cookie = parts.join('=');
+
+			if (key && key === name) {
+				// If second argument (value) is a function it's a converter...
+				result = read(cookie, value);
+				break;
+			}
+
+			// Prevent storing a cookie that we couldn't decode.
+			if (!key && (cookie = read(cookie)) !== undefined) {
+				result[name] = cookie;
+			}
+		}
+
+		return result;
+	};
+
+	config.defaults = {};
+
+	$.removeCookie = function (key, options) {
+		if ($.cookie(key) === undefined) {
+			return false;
+		}
+
+		// Must not alter options, thus extending a fresh object...
+		$.cookie(key, '', $.extend({}, options, { expires: -1 }));
+		return !$.cookie(key);
+	};
+
+}));

+ 4 - 4
dbsyncer-web/src/main/resources/static/plugins/js/loading-plus/loading-plus.js

@@ -1,4 +1,4 @@
-/***************************************************** 
+/*****************************************************
  *                                                 *#*
  * @项目:loadingT                                   *#*
  * @作者:AE86                                       *#*
@@ -15,7 +15,7 @@ $.loadingT = {
 		var content = '<i class="fa fa-spin fa-3x fa-refresh"></i>';
 		var $loadingT = $(".loadingT");
 		$loadingT.html("<div class='loading-indicator' unselectable='on' onselectstart='return false;'>"+title+content+"</div>");
-		$loadingT.css({ "height":$(document.body).height()+'px' });
+		// $loadingT.css({ "height":$(document.body).height()+'px' });
 		var $indicato = $(".loading-indicator");
 		$indicato.css({ "margin-top":($(window).height() / 2) -($indicato.height() / 2) });
 	}
@@ -28,7 +28,7 @@ jQuery.extend({
 		if(a){ $(".loadingT").fadeIn(300);}else{$(".loadingT").fadeOut(300);}
 	},
 	resetIndicato:function(){
-		$(".loadingT").css({ "height":$(document.body).height()+'px' });
+		// $(".loadingT").css({ "height":$(document.body).height()+'px' });
 		var $indicato = $(".loading-indicator");
 		$indicato.css({ "margin-top":($(window).height() / 2) -($indicato.height() / 2) });
 	}
@@ -36,4 +36,4 @@ jQuery.extend({
 //兼容浏览器缩放
 $(window).resize(function() {
 	$.resetIndicato();
-});
+});