getLatest(@Param("flowId") long flowId, @Param("numbers") long numbers);
+
+ int killFlowExecution(@Param("flowExecutionId") long flowExecutionId);
+
+ /**
+ * 获取所有需要删除的flow execution id
+ *
+ * 需要删除:flowId 不在列表中, 并且最后一次更新时间大于一小时
+ */
+ List getNeedDeleteFlowExecutionId(@NonNull List flowIds);
+
+ /**
+ * 通过id 删除 flow execution
+ */
+ void deleteFlowExecutionByIds(@NonNull List needDeleteFlowExecutionIds);
+
+ /**
+ * 获取全部的flow execution ids
+ */
+ List getAllFlowExecutionIds();
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/mapper/FlowMapper.java b/thain-core/src/main/java/com/xiaomi/thain/core/mapper/FlowMapper.java
new file mode 100644
index 00000000..19bd930f
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/mapper/FlowMapper.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.mapper;
+
+import com.xiaomi.thain.common.model.FlowModel;
+import com.xiaomi.thain.common.model.JobModel;
+import lombok.NonNull;
+import org.apache.ibatis.annotations.Param;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+/**
+ * Date 19-5-17 下午5:22
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+public interface FlowMapper {
+
+ /**
+ * 数据库插入flow,成功flowModel插入id
+ */
+ int addFlow(@NonNull FlowModel flowModel);
+
+ /**
+ * 更新
+ */
+ int updateFlow(@NonNull FlowModel flowModel);
+
+ int deleteFlow(long flowId);
+
+ @Nullable
+ FlowModel getFlow(long flowId);
+
+ /**
+ * 更新最后一次运行状态
+ */
+ int updateLastRunStatus(@Param("flowId") long flowId, @Param("lastRunStatus") int lastRunStatus);
+
+ int addJobList(@NonNull List jobModelList);
+
+ int invalidJobList(@Param("flowId") long flowId);
+
+ int deleteJob(long flowId);
+
+ int updateSchedulingStatus(@Param("flowId") long flowId, @Param("schedulingStatus") int schedulingStatus);
+
+ /**
+ * 获取所有的flow id
+ */
+ List getAllFlowIds();
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/mapper/JobExecutionMapper.java b/thain-core/src/main/java/com/xiaomi/thain/core/mapper/JobExecutionMapper.java
new file mode 100644
index 00000000..0187aeb6
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/mapper/JobExecutionMapper.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.mapper;
+
+import com.xiaomi.thain.common.model.JobExecutionModel;
+import lombok.NonNull;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * Date 19-5-17 下午5:22
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+public interface JobExecutionMapper {
+
+ int add(@NonNull JobExecutionModel jobExecutionModel);
+
+ int updateLogs(@Param("jobExecutionId") long jobExecutionId, @NonNull @Param("logs") String logs);
+
+ int updateStatus(@Param("jobExecutionId") long jobExecutionId, @Param("status") int status);
+
+ int updateCreateTime(@Param("jobExecutionId") long jobExecutionId);
+
+ /**
+ * 获取所有需要删除的Job execution id
+ *
+ * 需要删除:flow execution Id 不在列表中, 并且最后一次更新时间大于一小时
+ */
+ List getNeedDeleteJobExecutionIds(@NonNull List flowExecutionIds);
+
+ void deleteJobExecutionByIds(@NonNull List needDeleteJobExecutionIds);
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/mapper/JobMapper.java b/thain-core/src/main/java/com/xiaomi/thain/core/mapper/JobMapper.java
new file mode 100644
index 00000000..34381310
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/mapper/JobMapper.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.mapper;
+
+import com.xiaomi.thain.common.model.JobModel;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * Date 19-5-17 下午5:22
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+public interface JobMapper {
+
+ List getJobs(@Param("flowId") long flowId);
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/mapper/UserMapper.java b/thain-core/src/main/java/com/xiaomi/thain/core/mapper/UserMapper.java
new file mode 100644
index 00000000..aee3150c
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/mapper/UserMapper.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.mapper;
+
+import com.xiaomi.thain.core.entity.ThainUser;
+
+import java.util.List;
+
+/**
+ * Date 19-5-17 下午5:22
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+public interface UserMapper {
+
+ List getAdminUsers();
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/ProcessEngine.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/ProcessEngine.java
new file mode 100644
index 00000000..14226ee0
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/ProcessEngine.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.process;
+
+import static com.xiaomi.thain.common.constant.FlowSchedulingStatus.NOT_SET;
+import static com.xiaomi.thain.common.constant.FlowSchedulingStatus.SCHEDULING;
+
+import com.xiaomi.thain.common.exception.ThainException;
+import com.xiaomi.thain.common.exception.ThainMissRequiredArgumentsException;
+import com.xiaomi.thain.common.exception.ThainRuntimeException;
+import com.xiaomi.thain.common.model.FlowModel;
+import com.xiaomi.thain.common.model.JobModel;
+import com.xiaomi.thain.core.ThainFacade;
+import com.xiaomi.thain.core.config.DatabaseHandler;
+import com.xiaomi.thain.core.constant.FlowExecutionTriggerType;
+import com.xiaomi.thain.core.dao.*;
+import com.xiaomi.thain.core.process.runtime.executor.FlowExecutor;
+import com.xiaomi.thain.core.process.service.ComponentService;
+import com.xiaomi.thain.core.process.service.MailService;
+import com.xiaomi.thain.core.thread.pool.ThainThreadPool;
+import lombok.EqualsAndHashCode;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ibatis.io.Resources;
+import org.apache.ibatis.jdbc.ScriptRunner;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.LongFunction;
+
+import static com.xiaomi.thain.common.constant.FlowSchedulingStatus.NOT_SET;
+import static com.xiaomi.thain.common.constant.FlowSchedulingStatus.SCHEDULING;
+
+/**
+ * Date 19-5-17 下午2:09
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+@EqualsAndHashCode
+@Slf4j
+public class ProcessEngine {
+
+ @NonNull
+ public final String processEngineId;
+ @NonNull
+ public final ProcessEngineStorage processEngineStorage;
+ @NonNull
+ public final ThainFacade thainFacade;
+
+ private static final Map PROCESS_ENGINE_MAP = new ConcurrentHashMap<>();
+
+ private ProcessEngine(@NonNull ProcessEngineConfiguration processEngineConfiguration, @NonNull ThainFacade thainFacade)
+ throws ThainMissRequiredArgumentsException, SQLException, IOException {
+ this.thainFacade = thainFacade;
+ this.processEngineId = UUID.randomUUID().toString();
+ PROCESS_ENGINE_MAP.put(processEngineId, this);
+
+ LongFunction flowExecutionJobExecutionThreadPool = flowExecutionId -> ThainThreadPool.getInstance(
+ "thain-job-execution-thread[flowExecutionId:" + flowExecutionId + "]",
+ processEngineConfiguration.flowExecutionJobExecutionThreadPoolCoreSize,
+ processEngineConfiguration.flowExecutionJobExecutionThreadPoolMaximumSize,
+ processEngineConfiguration.flowExecutionJobExecutionThreadPoolKeepAliveSecond);
+ val flowExecutionThreadPool = ThainThreadPool.getInstance("thain-flowExecution-thread",
+ processEngineConfiguration.flowExecutionThreadPoolCoreSize,
+ processEngineConfiguration.flowExecutionThreadPoolMaximumSize,
+ processEngineConfiguration.flowExecutionThreadPoolKeepAliveSecond);
+
+ val sqlSessionFactory = DatabaseHandler.getSqlSessionFactory(processEngineConfiguration.dataSource);
+
+ switch (processEngineConfiguration.initLevel) {
+ case "1":
+ createTable(processEngineConfiguration.dataSource.getConnection());
+ initData(processEngineConfiguration.dataSource.getConnection());
+ break;
+ case "2":
+ initData(processEngineConfiguration.dataSource.getConnection());
+ break;
+ default:
+ }
+
+
+ val userDao = UserDao.getInstance(sqlSessionFactory);
+
+ val mailService = MailService.getInstance(processEngineConfiguration.mailHost,
+ processEngineConfiguration.mailSender,
+ processEngineConfiguration.mailSenderUsername,
+ processEngineConfiguration.mailSenderPassword,
+ userDao);
+
+ val flowDao = FlowDao.getInstance(sqlSessionFactory, mailService);
+ val flowExecutionDao = FlowExecutionDao.getInstance(sqlSessionFactory, mailService, processEngineConfiguration.dataReserveDays);
+ val jobDao = JobDao.getInstance(sqlSessionFactory, mailService);
+ val jobExecutionDao = JobExecutionDao.getInstance(sqlSessionFactory, mailService);
+
+ val componentService = ComponentService.getInstance();
+
+ processEngineStorage = ProcessEngineStorage.builder()
+ .flowExecutionJobExecutionThreadPool(flowExecutionJobExecutionThreadPool)
+ .flowExecutionThreadPool(flowExecutionThreadPool)
+ .processEngineId(processEngineId)
+ .flowDao(flowDao)
+ .flowExecutionDao(flowExecutionDao)
+ .jobDao(jobDao)
+ .jobExecutionDao(jobExecutionDao)
+ .mailService(mailService)
+ .componentService(componentService)
+ .build();
+ }
+
+ private void createTable(@NonNull Connection connection) throws IOException, SQLException {
+ ScriptRunner runner = new ScriptRunner(connection);
+ val driver = DriverManager.getDriver(connection.getMetaData().getURL()).getClass().getName();
+ if ("org.h2.Driver".equals(driver)) {
+ runner.runScript(Resources.getResourceAsReader("sql/h2/quartz.sql"));
+ runner.runScript(Resources.getResourceAsReader("sql/h2/thain.sql"));
+ } else if ("com.mysql.cj.jdbc.Driver".equals(driver)) {
+ runner.runScript(Resources.getResourceAsReader("sql/mysql/quartz.sql"));
+ runner.runScript(Resources.getResourceAsReader("sql/mysql/spring_session.sql"));
+ runner.runScript(Resources.getResourceAsReader("sql/mysql/thain.sql"));
+ }
+ }
+
+ private void initData(@NonNull Connection connection) throws IOException, SQLException {
+ ScriptRunner runner = new ScriptRunner(connection);
+ val driver = DriverManager.getDriver(connection.getMetaData().getURL()).getClass().getName();
+ if ("org.h2.Driver".equals(driver)) {
+ runner.runScript(Resources.getResourceAsReader("sql/h2/init_data.sql"));
+ } else if ("com.mysql.cj.jdbc.Driver".equals(driver)) {
+ runner.runScript(Resources.getResourceAsReader("sql/mysql/init_data.sql"));
+ }
+ }
+
+ /**
+ * 用id获取流程实例
+ */
+ public static ProcessEngine getInstance(@NonNull String processEngineId) {
+ return Optional.ofNullable(PROCESS_ENGINE_MAP.get(processEngineId)).orElseThrow(
+ () -> new ThainRuntimeException("Failed to obtain process instance"));
+ }
+
+ public static ProcessEngine newInstance(@NonNull ProcessEngineConfiguration processEngineConfiguration,
+ @NonNull ThainFacade thainFacade)
+ throws ThainMissRequiredArgumentsException, IOException, SQLException {
+ return new ProcessEngine(processEngineConfiguration, thainFacade);
+ }
+
+ /**
+ * 插入flow
+ * 成功返回 flow id
+ */
+ public Optional addFlow(@NonNull FlowModel flowModel, @NonNull List jobModelList) {
+ try {
+ int schedulingStatus = NOT_SET.code;
+ if (StringUtils.isNotBlank(flowModel.cron)) {
+ schedulingStatus = SCHEDULING.code;
+ }
+ val cloneFlowModel = flowModel.toBuilder().schedulingStatus(schedulingStatus).build();
+ processEngineStorage.flowDao.addFlow(cloneFlowModel, jobModelList);
+ return Optional.of(cloneFlowModel.id);
+ } catch (Exception e) {
+ log.error("addFlow:", e);
+ }
+ return Optional.empty();
+ }
+
+ public boolean updateFlow(@NonNull FlowModel flowModel, @NonNull List jobModelList) {
+ try {
+ int schedulingStatus = NOT_SET.code;
+ if (StringUtils.isNotBlank(flowModel.cron)) {
+ schedulingStatus = SCHEDULING.code;
+ }
+ val cloneFlowModel = flowModel.toBuilder().schedulingStatus(schedulingStatus).build();
+ processEngineStorage.flowDao.updateFlow(cloneFlowModel, jobModelList);
+ return true;
+ } catch (Exception e) {
+ log.error("updateFlow:", e);
+ }
+ return false;
+ }
+
+ /**
+ * 删除flow
+ * 返回删除job个数
+ */
+ public void deleteFlow(long flowId) {
+ processEngineStorage.flowDao.deleteFlow(flowId);
+ }
+
+ /**
+ * 手动触发一次
+ */
+ public void startProcess(long flowId) throws ThainException {
+ FlowExecutor.startProcess(flowId, processEngineStorage, FlowExecutionTriggerType.MANUAL);
+ }
+
+ /**
+ * 自动触发一次
+ */
+ public void schedulerStartProcess(long flowId) throws ThainException {
+ FlowExecutor.startProcess(flowId, processEngineStorage, FlowExecutionTriggerType.AUTOMATIC);
+ }
+
+ public String getFlowCron(long flowId) throws ThainException {
+ return processEngineStorage.flowDao
+ .getFlow(flowId).orElseThrow(() -> new ThainException("failed to obtain flow"))
+ .cron;
+ }
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/ProcessEngineConfiguration.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/ProcessEngineConfiguration.java
new file mode 100644
index 00000000..9040d57e
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/ProcessEngineConfiguration.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.process;
+
+import com.xiaomi.thain.common.exception.ThainMissRequiredArgumentsException;
+import lombok.Builder;
+import lombok.NonNull;
+import lombok.ToString;
+
+import javax.sql.DataSource;
+
+/**
+ * @author liangyongrui@xiaomi.com
+ * @date 19-5-16 下午8:35
+ */
+@ToString
+@Builder(builderMethodName = "doNotUseIt")
+public class ProcessEngineConfiguration {
+
+ /**
+ * 发送邮件相关属性
+ */
+ @NonNull
+ public final String mailHost;
+ @NonNull
+ public final String mailSender;
+ @NonNull
+ public final String mailSenderUsername;
+ @NonNull
+ public final String mailSenderPassword;
+
+ /**
+ * 数据源
+ */
+ @NonNull
+ public final DataSource dataSource;
+
+ /**
+ * flowExecution线程池
+ */
+ @NonNull
+ public final Integer flowExecutionThreadPoolCoreSize;
+ @NonNull
+ public final Integer flowExecutionThreadPoolMaximumSize;
+ @NonNull
+ public final Long flowExecutionThreadPoolKeepAliveSecond;
+
+ /**
+ * 每个flowExecution的jobExecution线程池
+ */
+ @NonNull
+ public final Integer flowExecutionJobExecutionThreadPoolCoreSize;
+ @NonNull
+ public final Integer flowExecutionJobExecutionThreadPoolMaximumSize;
+ @NonNull
+ public final Long flowExecutionJobExecutionThreadPoolKeepAliveSecond;
+
+ /**
+ * 数据保留天数
+ */
+ @NonNull
+ public final Integer dataReserveDays;
+
+ @NonNull
+ public final String initLevel;
+
+ public static ProcessEngineConfigurationBuilder builder() throws ThainMissRequiredArgumentsException {
+ return doNotUseIt();
+ }
+
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/ProcessEngineStorage.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/ProcessEngineStorage.java
new file mode 100644
index 00000000..01c3f7f5
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/ProcessEngineStorage.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.process;
+
+import com.xiaomi.thain.common.exception.ThainMissRequiredArgumentsException;
+import com.xiaomi.thain.core.dao.FlowDao;
+import com.xiaomi.thain.core.dao.FlowExecutionDao;
+import com.xiaomi.thain.core.dao.JobDao;
+import com.xiaomi.thain.core.dao.JobExecutionDao;
+import com.xiaomi.thain.core.process.runtime.notice.MailNotice;
+import com.xiaomi.thain.core.process.service.ComponentService;
+import com.xiaomi.thain.core.process.service.MailService;
+import com.xiaomi.thain.core.thread.pool.ThainThreadPool;
+import lombok.Builder;
+import lombok.NonNull;
+import lombok.ToString;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.LongFunction;
+
+/**
+ * Date 19-5-31 下午7:45
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+@ToString
+@Builder(builderMethodName = "doNotUseIt")
+public final class ProcessEngineStorage {
+
+ @NonNull
+ public final ThainThreadPool flowExecutionThreadPool;
+ @NonNull
+ public final String processEngineId;
+ @NonNull
+ public final FlowDao flowDao;
+ @NonNull
+ public final FlowExecutionDao flowExecutionDao;
+ @NonNull
+ public final JobDao jobDao;
+ @NonNull
+ public final JobExecutionDao jobExecutionDao;
+ @NonNull
+ public final MailService mailService;
+ @NonNull
+ public final ComponentService componentService;
+ @NonNull
+ private final LongFunction flowExecutionJobExecutionThreadPool;
+
+ public MailNotice getMailNotice(@NonNull String noticeEmail) {
+ return MailNotice.getInstance(mailService, noticeEmail);
+ }
+
+ public ThainThreadPool flowExecutionJobThreadPool(long flowExecutionId) {
+ return flowExecutionJobExecutionThreadPool.apply(flowExecutionId);
+ }
+
+ public static ProcessEngineStorageBuilder builder() throws ThainMissRequiredArgumentsException {
+ return doNotUseIt();
+ }
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/component/tools/impl/ComponentToolsImpl.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/component/tools/impl/ComponentToolsImpl.java
new file mode 100644
index 00000000..4d318d1d
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/component/tools/impl/ComponentToolsImpl.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.process.component.tools.impl;
+
+import com.xiaomi.thain.common.model.JobModel;
+import com.xiaomi.thain.common.utils.HttpUtils;
+import com.xiaomi.thain.component.tools.ComponentTools;
+import com.xiaomi.thain.core.constant.LogLevel;
+import com.xiaomi.thain.core.process.ProcessEngineStorage;
+import com.xiaomi.thain.core.process.runtime.log.JobExecutionLogHandler;
+import com.xiaomi.thain.core.process.runtime.storage.FlowExecutionStorage;
+import com.xiaomi.thain.core.process.service.MailService;
+import lombok.NonNull;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Optional;
+import javax.mail.MessagingException;
+
+/**
+ * Date 19-5-30 下午4:31
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+public class ComponentToolsImpl implements ComponentTools {
+
+ @NonNull
+ private final JobModel jobModel;
+ @NonNull
+ private final FlowExecutionStorage flowExecutionStorage;
+ @NonNull
+ private final JobExecutionLogHandler log;
+ @NonNull
+ private final MailService mailService;
+ @NonNull
+ private final long jobExecutionId;
+
+ public ComponentToolsImpl(@NonNull JobModel jobModel,
+ long jobExecutionId,
+ long flowExecutionId,
+ @NonNull ProcessEngineStorage processEngineStorage) {
+ this.jobExecutionId = jobExecutionId;
+ this.mailService = processEngineStorage.mailService;
+ this.jobModel = jobModel;
+ this.flowExecutionStorage = FlowExecutionStorage.getInstance(flowExecutionId);
+ this.log = JobExecutionLogHandler.getInstance(jobExecutionId, processEngineStorage);
+ }
+
+ @Override
+ public void sendMail(@NonNull String[] to, @NonNull String subject, @NonNull String content) throws IOException, MessagingException {
+ mailService.send(to, subject, content);
+ }
+
+ /**
+ * 保存当前节点产生的数据
+ *
+ * @param key 数据的key
+ * @param value 数据的value
+ */
+ @Override
+ public void putStorage(@NonNull final String key, @NonNull final Object value) {
+ flowExecutionStorage.put(jobModel.name, key, value);
+ }
+
+ /**
+ * 获取流程已经产生的数据
+ *
+ * @param jobName 节点名称
+ * @param key key
+ * @param 自动强制转换
+ * @return 返回对应值的Optional
+ */
+ @Override
+ public Optional getStorageValue(@NonNull final String jobName, @NonNull final String key) {
+ return flowExecutionStorage.get(jobName, key);
+ }
+
+ /**
+ * 获取流程已经产生的数据
+ *
+ * @param jobName 节点名称
+ * @param key key
+ * @param defaultValue 默认值
+ * @param 自动强制转换
+ * @return 返回对应值, 值不存在则返回defaultValue
+ */
+ @Override
+ public T getStorageValueOrDefault(@NonNull final String jobName, @NonNull final String key, @NonNull final T defaultValue) {
+ final Optional optional = getStorageValue(jobName, key);
+ return optional.orElse(defaultValue);
+ }
+
+ /**
+ * 增加debug日志
+ */
+ @Override
+ public void addDebugLog(@NonNull String content) {
+ log.add(content, LogLevel.DEBUG);
+ }
+
+ /**
+ * 增加info日志
+ */
+ @Override
+ public void addInfoLog(@NonNull String content) {
+ log.add(content, LogLevel.INFO);
+ }
+
+ /**
+ * 增加warning日志
+ */
+ @Override
+ public void addWarnLog(@NonNull String content) {
+ log.add(content, LogLevel.WARN);
+ }
+
+ /**
+ * 增加error日志
+ */
+ @Override
+ public void addErrorLog(@NonNull String content) {
+ log.add(content, LogLevel.ERROR);
+ }
+
+ @Override
+ public String httpGet(@NonNull String url, @NonNull Map data) throws IOException {
+ return HttpUtils.get(url, data);
+ }
+
+ @Override
+ public String httpPost(@NonNull String url, @NonNull Map headers, @NonNull Map data)
+ throws IOException {
+ return HttpUtils.post(url, headers, data);
+ }
+
+ @Override
+ public long getJobExecutionId() {
+ return jobExecutionId;
+ }
+
+ @Override
+ public Map getStorage() {
+ return flowExecutionStorage.storageMap;
+ }
+
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/checker/JobConditionChecker.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/checker/JobConditionChecker.java
new file mode 100644
index 00000000..bb2b5e24
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/checker/JobConditionChecker.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.process.runtime.checker;
+
+import com.xiaomi.thain.common.exception.ThainRuntimeException;
+import com.xiaomi.thain.core.process.runtime.storage.FlowExecutionStorage;
+import lombok.NonNull;
+import lombok.val;
+
+import javax.annotation.Nullable;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+/**
+ * Date 19-5-17 下午12:38
+ * 判断job的condition是否合法
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+public class JobConditionChecker {
+
+ private static final String REGEX = "^(.|\\s)*?(>|<|>=|<=|==|!=)\\s*?(\\d*)$";
+ private static final Pattern PATTERN = Pattern.compile(REGEX);
+
+ @NonNull
+ private final FlowExecutionStorage flowExecutionStorage;
+
+ private JobConditionChecker(final long flowExecutionId) {
+ this.flowExecutionStorage = FlowExecutionStorage.getInstance(flowExecutionId);
+ }
+
+ public static JobConditionChecker getInstance(final long flowExecutionId) {
+ return new JobConditionChecker(flowExecutionId);
+ }
+
+ /**
+ * 判断条件是否可执行
+ */
+ public boolean executable(@Nullable String condition) {
+ return Optional.ofNullable(condition)
+ .map(t -> t.split("&&|\\|\\|"))
+ .map(Arrays::stream).orElseGet(Stream::empty)
+ .map(String::trim).noneMatch(t -> {
+ if (t.isEmpty()) {
+ return false;
+ }
+ val end = t.indexOf('.');
+ val name = end == -1 ? t : t.substring(0, end);
+ if (!flowExecutionStorage.finished(name)) {
+ return true;
+ }
+ if (end != -1) {
+ val predicate = t.substring(end).trim();
+ Matcher matcher = PATTERN.matcher(predicate);
+ if (matcher.find()) {
+ val left = matcher.group(1).trim();
+ val op = matcher.group(2);
+ val right = Integer.valueOf(matcher.group(3));
+ return !calculate(name, left, op, right);
+ } else {
+ return false;
+ }
+ }
+ return false;
+ });
+ }
+
+ private boolean calculate(String name, String left, String op, Integer right) {
+ try {
+ long leftValue = Long.parseLong(String.valueOf(flowExecutionStorage.get(name, left)
+ .orElseThrow(() -> new ThainRuntimeException("The calculated value does not exist"))));
+ switch (op) {
+ case "==":
+ return leftValue == right;
+ case "!=":
+ return leftValue != right;
+ case ">":
+ return leftValue > right;
+ case "<":
+ return leftValue < right;
+ case ">=":
+ return leftValue >= right;
+ case "<=":
+ return leftValue <= right;
+ default:
+ return false;
+ }
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/executor/FlowExecutor.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/executor/FlowExecutor.java
new file mode 100644
index 00000000..16a280b9
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/executor/FlowExecutor.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+
+package com.xiaomi.thain.core.process.runtime.executor;
+
+import com.google.common.collect.ImmutableList;
+import com.xiaomi.thain.common.constant.FlowExecutionStatus;
+import com.xiaomi.thain.common.constant.FlowLastRunStatus;
+import com.xiaomi.thain.common.constant.JobExecutionStatus;
+import com.xiaomi.thain.common.exception.CreateFlowExecutionException;
+import com.xiaomi.thain.common.exception.ThainException;
+import com.xiaomi.thain.common.exception.ThainFlowRunningException;
+import com.xiaomi.thain.common.exception.ThainRuntimeException;
+import com.xiaomi.thain.common.model.FlowExecutionModel;
+import com.xiaomi.thain.common.model.FlowModel;
+import com.xiaomi.thain.common.model.JobExecutionModel;
+import com.xiaomi.thain.common.model.JobModel;
+import com.xiaomi.thain.core.constant.FlowExecutionTriggerType;
+import com.xiaomi.thain.core.process.ProcessEngine;
+import com.xiaomi.thain.core.process.ProcessEngineStorage;
+import com.xiaomi.thain.core.process.runtime.checker.JobConditionChecker;
+import com.xiaomi.thain.core.process.runtime.executor.service.FlowExecutionService;
+import com.xiaomi.thain.core.process.runtime.notice.HttpNotice;
+import com.xiaomi.thain.core.process.runtime.storage.FlowExecutionStorage;
+import com.xiaomi.thain.core.thread.pool.ThainThreadPool;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+
+import java.net.InetAddress;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import static java.util.stream.Collectors.*;
+
+/**
+ * 任务执行器: 创建执行任务,管理执行流程
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+@Slf4j
+public class FlowExecutor {
+
+ private final long flowExecutionId;
+ @NonNull
+ private final FlowModel flowModel;
+ @NonNull
+ private final ProcessEngineStorage processEngineStorage;
+ @NonNull
+ private final JobConditionChecker jobConditionChecker;
+ @NonNull
+ private final FlowExecutionStorage flowExecutionStorage;
+ @NonNull
+ private final FlowExecutionService flowExecutionService;
+ @NonNull
+ private final HttpNotice httpNotice;
+ @NonNull
+ private final ThainThreadPool flowExecutionJobThreadPool;
+ @NonNull
+ private final Map jobExecutionModelMap;
+
+ /**
+ * 当前未执行的节点
+ */
+ @NonNull
+ private Collection notExecutedJobsPool;
+ /**
+ * 监控节点是否执行完,也可用于中断任务 或 中断节点
+ */
+ @NonNull
+ private final Map> jobFutureMap;
+ /**
+ * 监控节点是否执行完
+ */
+ @NonNull
+ private final Queue> jobFutureQueue;
+
+ /**
+ * 是否killed
+ */
+ private long newFlowExecutor(long flowId, @NonNull FlowExecutionTriggerType flowExecutionTriggerType) throws ThainException {
+ try {
+ int triggerTypeCode;
+ switch (flowExecutionTriggerType) {
+ case MANUAL:
+ triggerTypeCode = 1;
+ break;
+ case AUTOMATIC:
+ triggerTypeCode = 2;
+ break;
+ default:
+ throw new ThainRuntimeException("The trigger type is unknown");
+ }
+
+ val flowExecutionModel = FlowExecutionModel.builder()
+ .flowId(flowId)
+ .hostInfo(InetAddress.getLocalHost().toString())
+ .status(FlowExecutionStatus.RUNNING.code)
+ .triggerType(triggerTypeCode).build();
+
+ //创建任务失败
+ processEngineStorage.flowExecutionDao.addFlowExecution(flowExecutionModel);
+ if (flowExecutionModel.id == 0) {
+ throw new ThainException("Failed to insert into database");
+ }
+ return flowExecutionModel.id;
+ } catch (Exception e) {
+ try {
+ processEngineStorage.mailService.sendSeriousError(
+ "FlowExecution create failed, flowId:" + flowId + "detail message:" + ExceptionUtils.getStackTrace(e));
+ } catch (Exception ex) {
+ throw new CreateFlowExecutionException(e);
+ }
+ throw new CreateFlowExecutionException(e);
+ }
+ }
+
+ /**
+ * 获取FlowExecutionExecutor实例
+ * effect:为了获取flowExecutionId 会在数据库中创建一个flowExecution
+ */
+ private FlowExecutor(@NonNull FlowModel flowModel, @NonNull ProcessEngineStorage processEngineStorage,
+ @NonNull FlowExecutionTriggerType flowExecutionTriggerType)
+ throws ThainException {
+ this.processEngineStorage = processEngineStorage;
+ this.flowModel = flowModel;
+ this.jobFutureMap = new ConcurrentHashMap<>();
+ this.jobFutureQueue = new ConcurrentLinkedQueue<>();
+ try {
+ this.flowExecutionId = newFlowExecutor(flowModel.id, flowExecutionTriggerType);
+ val jobModelList = processEngineStorage.jobDao.getJobs(flowModel.id).orElseThrow(CreateFlowExecutionException::new);
+ this.flowExecutionService = FlowExecutionService.getInstance(flowExecutionId, flowModel, processEngineStorage);
+ this.notExecutedJobsPool = ImmutableList.copyOf(jobModelList);
+ this.jobConditionChecker = JobConditionChecker.getInstance(flowExecutionId);
+ this.flowExecutionStorage = FlowExecutionStorage.getInstance(flowExecutionId);
+ this.httpNotice = HttpNotice.getInstance(flowModel.callbackUrl, flowModel.id, flowExecutionId);
+ this.flowExecutionJobThreadPool = processEngineStorage.flowExecutionJobThreadPool(flowExecutionId);
+ this.jobExecutionModelMap = jobModelList.stream().collect(toMap(t -> t.id, t -> {
+ val jobExecutionModel = JobExecutionModel.builder()
+ .jobId(t.id)
+ .flowExecutionId(flowExecutionId)
+ .status(JobExecutionStatus.NEVER.code)
+ .build();
+ processEngineStorage.jobExecutionDao.add(jobExecutionModel);
+ return jobExecutionModel;
+ }));
+ } catch (Exception e) {
+ log.error("", e);
+ throw new CreateFlowExecutionException(flowModel.id, e.getMessage());
+ }
+ }
+
+ /**
+ * 开始执行流程,产生一个flowExecution,成功后异步执行start方法
+ */
+ public static void startProcess(long flowId,
+ @NonNull ProcessEngineStorage processEngineStorage,
+ @NonNull FlowExecutionTriggerType flowExecutionTriggerType) throws ThainException {
+ val flowModel = processEngineStorage.flowDao.getFlow(flowId).orElseThrow(() -> new ThainException("flow does not exist"));
+ val flowLastRunStatus = FlowLastRunStatus.getInstance(flowModel.lastRunStatus);
+ if (flowLastRunStatus == FlowLastRunStatus.RUNNING) {
+ throw new ThainFlowRunningException(flowId);
+ }
+ val flowExecutionService = new FlowExecutor(flowModel, processEngineStorage, flowExecutionTriggerType);
+ CompletableFuture.runAsync(flowExecutionService::start, processEngineStorage.flowExecutionThreadPool);
+ }
+
+ /**
+ * 流程执行入口
+ */
+ private void start() {
+ try {
+ flowExecutionService.startFlowExecution();
+ httpNotice.sendStart();
+ if (flowModel.slaDuration > 0) {
+ ProcessEngine.getInstance(processEngineStorage.processEngineId)
+ .thainFacade
+ .schedulerEngine
+ .addSla(flowExecutionId, flowModel);
+ }
+ runExecutableJobs();
+ while (!jobFutureQueue.isEmpty()) {
+ jobFutureQueue.poll().join();
+ }
+ } catch (Exception e) {
+ log.error("", e);
+ flowExecutionService.addError(ExceptionUtils.getStackTrace(e));
+ } finally {
+ flowExecutionService.endFlowExecution();
+ switch (flowExecutionService.getFlowEndStatus()) {
+ case SUCCESS:
+ httpNotice.sendSuccess();
+ break;
+ default:
+ httpNotice.sendError(flowExecutionService.getErrorMessage());
+ }
+ FlowExecutionStorage.drop(flowExecutionId);
+ }
+ }
+
+ /**
+ * 执行可以执行的节点
+ */
+ private synchronized void runExecutableJobs() {
+ val flowExecutionModel = processEngineStorage.flowExecutionDao.getFlowExecution(flowExecutionId).orElseThrow(
+ () -> new ThainRuntimeException("Failed to read FlowExecution information, flowExecutionId: " + flowExecutionId));
+ val flowExecutionStatus = FlowExecutionStatus.getInstance(flowExecutionModel.status);
+ if (flowExecutionStatus == FlowExecutionStatus.KILLED) {
+ flowExecutionService.killed();
+ return;
+ }
+ val executableJobs = getExecutableJobs();
+ executableJobs.forEach(job -> {
+ val future = CompletableFuture.runAsync(() -> {
+ flowExecutionService.addInfo("Start executing the job [" + job.name + "]");
+ if (JobExecutor.start(flowExecutionId, job, jobExecutionModelMap.get(job.id), processEngineStorage)) {
+ flowExecutionService.addInfo("Execute job[" + job.name + "] complete");
+ flowExecutionStorage.addFinishJob(job.name);
+ } else {
+ flowExecutionService.addError("Job[" + job.name + "] exception");
+ }
+ runExecutableJobs();
+ }, flowExecutionJobThreadPool);
+ jobFutureMap.put(job.name, future);
+ jobFutureQueue.add(future);
+ });
+ }
+
+ private Collection getExecutableJobs() {
+ val executableJobs = notExecutedJobsPool.stream()
+ .filter(t -> jobConditionChecker.executable(t.condition)).collect(toSet());
+ notExecutedJobsPool = notExecutedJobsPool.stream()
+ .filter(t -> !executableJobs.contains(t)).collect(toList());
+ return executableJobs;
+ }
+
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/executor/JobExecutor.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/executor/JobExecutor.java
new file mode 100644
index 00000000..a458de51
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/executor/JobExecutor.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+
+package com.xiaomi.thain.core.process.runtime.executor;
+
+import com.xiaomi.thain.common.exception.JobExecuteException;
+import com.xiaomi.thain.common.exception.ThainException;
+import com.xiaomi.thain.common.model.JobExecutionModel;
+import com.xiaomi.thain.common.model.JobModel;
+import com.xiaomi.thain.component.tools.ComponentTools;
+import com.xiaomi.thain.core.process.ProcessEngineStorage;
+import com.xiaomi.thain.core.process.component.tools.impl.ComponentToolsImpl;
+import com.xiaomi.thain.core.process.runtime.executor.service.JobExecutionService;
+import com.xiaomi.thain.core.process.runtime.notice.HttpNotice;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+
+import java.util.Optional;
+
+/**
+ * 节点执行类
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+@Slf4j
+public class JobExecutor {
+
+ @NonNull
+ private final JobModel jobModel;
+ private final long jobExecutionModelId;
+ private final long flowExecutionId;
+
+ @NonNull
+ private final ProcessEngineStorage processEngineStorage;
+ @NonNull
+ private final JobExecutionService jobExecutionService;
+ @NonNull
+ private final HttpNotice httpNotice;
+
+ private JobExecutor(long flowExecutionId,
+ @NonNull JobModel jobModel,
+ @NonNull JobExecutionModel jobExecutionModel,
+ @NonNull ProcessEngineStorage processEngineStorage) {
+ this.jobModel = jobModel;
+ this.flowExecutionId = flowExecutionId;
+ this.processEngineStorage = processEngineStorage;
+ this.jobExecutionModelId = jobExecutionModel.id;
+ this.jobExecutionService = JobExecutionService.getInstance(jobExecutionModelId, jobModel.name, processEngineStorage);
+ this.httpNotice = HttpNotice.getInstance(jobModel.callbackUrl, jobModel.flowId, flowExecutionId);
+ }
+
+ /**
+ * 执行job, 返回是否执行完成
+ */
+ public static boolean start(long flowExecutionId,
+ @NonNull JobModel jobModel,
+ @NonNull JobExecutionModel jobExecutionModel,
+ @NonNull ProcessEngineStorage processEngineStorage) {
+ try {
+ val jobExecutor = new JobExecutor(flowExecutionId, jobModel, jobExecutionModel, processEngineStorage);
+ return jobExecutor.run();
+ } catch (Exception e) {
+ log.error("", e);
+ }
+ return false;
+ }
+
+ private boolean run() {
+ try {
+ jobExecutionService.startJobExecution();
+ httpNotice.sendStart();
+ execute();
+ httpNotice.sendSuccess();
+ return true;
+ } catch (JobExecuteException e) {
+ jobExecutionService.addError("Abort with: " + ExceptionUtils.getRootCauseMessage(e));
+ httpNotice.sendError(ExceptionUtils.getRootCauseMessage(e));
+ log.warn(ExceptionUtils.getRootCauseMessage(e));
+ } catch (Exception e) {
+ jobExecutionService.addError("Abort with: " + ExceptionUtils.getRootCauseMessage(e));
+ httpNotice.sendError(ExceptionUtils.getRootCauseMessage(e));
+ log.error("", e);
+ } finally {
+ try {
+ jobExecutionService.endJobExecution();
+ } catch (Exception e) {
+ try {
+ processEngineStorage.mailService.sendSeriousError(
+ "Failed to modify job status,detail message:" + ExceptionUtils.getStackTrace(e));
+ } catch (Exception ex) {
+ log.error("", ex);
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 执行组件
+ */
+ private void execute() throws ThainException {
+ val clazz = processEngineStorage.componentService.getComponentClass(jobModel.component)
+ .orElseThrow(() -> new ThainException("component does not exist"));
+ try {
+ val instance = clazz.getConstructor().newInstance();
+ val fields = instance.getClass().getDeclaredFields();
+ for (val field : fields) {
+ field.setAccessible(true);
+ if (ComponentTools.class.isAssignableFrom(field.getType())) {
+ field.set(instance, new ComponentToolsImpl(jobModel, jobExecutionModelId, flowExecutionId, processEngineStorage));
+ continue;
+ }
+ if (field.getType().isAssignableFrom(String.class)) {
+ val v = Optional.ofNullable(jobModel.properties.get(field.getName()));
+ if (v.isPresent()) {
+ field.set(instance, v.get());
+ }
+ }
+ }
+ val method = clazz.getDeclaredMethod("run");
+ method.setAccessible(true);
+ method.invoke(instance);
+ } catch (Exception e) {
+ throw new JobExecuteException(e);
+ }
+ }
+
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/executor/service/FlowExecutionService.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/executor/service/FlowExecutionService.java
new file mode 100644
index 00000000..641b32d0
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/executor/service/FlowExecutionService.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.process.runtime.executor.service;
+
+import com.xiaomi.thain.common.constant.FlowExecutionStatus;
+import com.xiaomi.thain.common.constant.FlowLastRunStatus;
+import com.xiaomi.thain.common.exception.ThainException;
+import com.xiaomi.thain.common.model.FlowModel;
+import com.xiaomi.thain.core.dao.FlowExecutionDao;
+import com.xiaomi.thain.core.process.ProcessEngine;
+import com.xiaomi.thain.core.process.ProcessEngineStorage;
+import com.xiaomi.thain.core.process.runtime.log.FlowExecutionLogHandler;
+import com.xiaomi.thain.core.process.runtime.notice.MailNotice;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.apache.logging.log4j.util.Strings;
+
+import javax.mail.MessagingException;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Date 19-5-21 上午10:46
+ * 任务服务类,对不影响任务执行的方法进行管理,如:日志,状态等
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+@Slf4j
+public class FlowExecutionService {
+
+ public final long flowExecutionId;
+
+ @NonNull
+ private final FlowExecutionLogHandler flowExecutionLogHandler;
+ @NonNull
+ private final MailNotice mailNotice;
+ @NonNull
+ private final FlowService flowService;
+ @NonNull
+ private final FlowExecutionDao flowExecutionDao;
+ @NonNull
+ private final ProcessEngineStorage processEngineStorage;
+ @NonNull
+ private final FlowModel flowModel;
+
+ /**
+ * 如果是异常结束,异常信息.
+ * 正常结束时,errorMessage为""
+ */
+ @Getter
+ @NonNull
+ private String errorMessage = "";
+
+ /**
+ * 流程结束状态
+ */
+ @Getter
+ @NonNull
+ private FlowLastRunStatus flowEndStatus = FlowLastRunStatus.SUCCESS;
+
+ @NonNull
+ private FlowExecutionStatus flowExecutionEndStatus = FlowExecutionStatus.SUCCESS;
+
+ private static final Map FLOW_EXECUTION_SERVICE_MAP = new ConcurrentHashMap<>();
+
+ private FlowExecutionService(long flowExecutionId,
+ @NonNull FlowModel flowModel,
+ @NonNull ProcessEngineStorage processEngineStorage) {
+ this.processEngineStorage = processEngineStorage;
+ this.flowExecutionId = flowExecutionId;
+ this.flowService = FlowService.getInstance(flowModel.id, processEngineStorage);
+ this.flowExecutionLogHandler = FlowExecutionLogHandler.getInstance(flowExecutionId, processEngineStorage);
+ this.flowExecutionDao = processEngineStorage.flowExecutionDao;
+ this.mailNotice = processEngineStorage.getMailNotice(flowModel.callbackEmail);
+ this.flowModel = flowModel;
+ }
+
+ public static FlowExecutionService getInstance(long flowExecutionId,
+ @NonNull FlowModel flowModel,
+ @NonNull ProcessEngineStorage processEngineStorage) {
+ return FLOW_EXECUTION_SERVICE_MAP.computeIfAbsent(flowExecutionId,
+ id -> new FlowExecutionService(id, flowModel, processEngineStorage));
+ }
+
+ /**
+ * 开始任务
+ */
+ public void startFlowExecution() {
+ try {
+ flowService.startFlow();
+ flowExecutionLogHandler.addInfo("begin to execute flow:" + flowExecutionId);
+ } catch (Exception e) {
+ log.error("", e);
+ }
+ }
+
+ /**
+ * 添加错误
+ */
+ public void addError(@NonNull String message) {
+ this.errorMessage = message;
+ flowEndStatus = FlowLastRunStatus.ERROR;
+ flowExecutionEndStatus = FlowExecutionStatus.ERROR;
+ }
+
+ /**
+ * 添加错误
+ */
+ public void killed() {
+ this.errorMessage = "manual kill";
+ flowEndStatus = FlowLastRunStatus.KILLED;
+ flowExecutionEndStatus = FlowExecutionStatus.KILLED;
+ }
+
+ /**
+ * 结束任务
+ */
+ public void endFlowExecution() {
+ try {
+ switch (flowEndStatus) {
+ case SUCCESS:
+ flowExecutionLogHandler.endSuccess();
+ break;
+ default:
+ flowExecutionLogHandler.endError(errorMessage);
+ mailNotice.sendError(errorMessage);
+ checkContinuousFailure();
+
+ }
+ processEngineStorage.flowExecutionDao.updateFlowExecutionStatus(flowExecutionId, flowExecutionEndStatus.code);
+ flowService.endFlow(flowEndStatus);
+ close();
+ } catch (Exception e) {
+ log.error("", e);
+ }
+ }
+
+ public void addInfo(@NonNull String s) {
+ flowExecutionLogHandler.addInfo(s);
+ }
+
+ /**
+ * 连续失败暂停任务
+ */
+ private void checkContinuousFailure() throws ThainException, IOException, MessagingException {
+ if (flowModel.pauseContinuousFailure > 0) {
+ val latest = flowExecutionDao.getLatest(flowModel.id, flowModel.pauseContinuousFailure).orElseGet(Collections::emptyList);
+ val count = latest.stream().filter(t -> FlowExecutionStatus.getInstance(t.status) == FlowExecutionStatus.ERROR).count();
+ if (count >= flowModel.pauseContinuousFailure - 1) {
+ ProcessEngine.getInstance(processEngineStorage.processEngineId).thainFacade.pauseFlow(flowModel.id);
+ if (Strings.isNotBlank(flowModel.emailContinuousFailure)) {
+ processEngineStorage.mailService.send(
+ flowModel.emailContinuousFailure.trim().split(","),
+ "Thain 任务连续失败通知",
+ "您的任务:" + flowModel.name + ", 连续失败了" + flowModel.pauseContinuousFailure + "次,任务已经暂停。最近一次失败原因:" + errorMessage
+ );
+ }
+ }
+ }
+ }
+
+ private void close() {
+ FLOW_EXECUTION_SERVICE_MAP.remove(flowExecutionId);
+ }
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/executor/service/FlowService.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/executor/service/FlowService.java
new file mode 100644
index 00000000..bd3494c8
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/executor/service/FlowService.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.process.runtime.executor.service;
+
+import com.xiaomi.thain.common.constant.FlowLastRunStatus;
+import com.xiaomi.thain.core.dao.FlowDao;
+import com.xiaomi.thain.core.process.ProcessEngineStorage;
+import lombok.NonNull;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Date 19-5-21 上午10:46
+ * 不影响流程执行的flow相关操作
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+public class FlowService {
+
+ public final long flowId;
+
+ @NonNull
+ public final FlowDao flowDao;
+
+ private FlowService(long flowId,
+ @NonNull ProcessEngineStorage processEngineStorage) {
+ this.flowId = flowId;
+ this.flowDao = processEngineStorage.flowDao;
+
+ }
+
+ public static FlowService getInstance(long flowId,
+ @NonNull ProcessEngineStorage processEngineStorage) {
+ return new FlowService(flowId, processEngineStorage);
+ }
+
+ /**
+ * 开始运行flow
+ * 设置当前的flow状态为 正在运行
+ */
+ public void startFlow() {
+ flowDao.updateLastRunStatus(flowId, FlowLastRunStatus.RUNNING);
+ }
+
+ public void endFlow(@NonNull FlowLastRunStatus endStatus) {
+ flowDao.updateLastRunStatus(flowId, endStatus);
+ }
+
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/executor/service/JobExecutionService.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/executor/service/JobExecutionService.java
new file mode 100644
index 00000000..9ae2cea5
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/executor/service/JobExecutionService.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.process.runtime.executor.service;
+
+import com.xiaomi.thain.common.constant.JobExecutionStatus;
+import com.xiaomi.thain.core.constant.LogLevel;
+import com.xiaomi.thain.core.dao.JobExecutionDao;
+import com.xiaomi.thain.core.process.ProcessEngineStorage;
+import com.xiaomi.thain.core.process.runtime.log.JobExecutionLogHandler;
+import lombok.Getter;
+import lombok.NonNull;
+
+
+/**
+ * jobExecution服务类,对不影响任务执行的方法进行管理,如:日志,状态等
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+public class JobExecutionService {
+
+ @NonNull
+ private final JobExecutionLogHandler jobExecutionLogHandler;
+ @NonNull
+ private final JobExecutionDao jobExecutionDao;
+ private final long jobExecutionId;
+ @NonNull
+ private final String jobExecutionName;
+
+ /**
+ * 如果是异常结束,异常信息.
+ * 正常结束时,errorMessage为""
+ */
+ @Getter
+ private String errorMessage = "";
+
+ /**
+ * 流程结束状态
+ */
+ @Getter
+ @NonNull
+ private JobExecutionStatus endStatus = JobExecutionStatus.SUCCESS;
+
+ private JobExecutionService(long jobExecutionId,
+ @NonNull String jobExecutionName,
+ @NonNull ProcessEngineStorage processEngineStorage) {
+ this.jobExecutionLogHandler = JobExecutionLogHandler.getInstance(jobExecutionId, processEngineStorage);
+ this.jobExecutionDao = processEngineStorage.jobExecutionDao;
+ this.jobExecutionId = jobExecutionId;
+ this.jobExecutionName = jobExecutionName;
+ }
+
+ public static JobExecutionService getInstance(long jobExecutionId, @NonNull String jobExecutionName,
+ @NonNull ProcessEngineStorage processEngineStorage) {
+ return new JobExecutionService(jobExecutionId, jobExecutionName, processEngineStorage);
+ }
+
+ public void startJobExecution() {
+ jobExecutionLogHandler.add("begin execute node:" + jobExecutionName, LogLevel.INFO);
+ jobExecutionDao.updateCreateTimeAndStatus(jobExecutionId, JobExecutionStatus.RUNNING);
+ }
+
+ public void endJobExecution() {
+ switch (endStatus) {
+ case ERROR:
+ jobExecutionLogHandler.add("executed abort with:" + errorMessage, LogLevel.ERROR);
+ break;
+ default:
+ jobExecutionLogHandler.add("executed completed", LogLevel.INFO);
+ }
+ jobExecutionLogHandler.close();
+ jobExecutionDao.updateStatus(jobExecutionId, endStatus);
+ }
+
+ public void addError(@NonNull String errorMessage) {
+ this.errorMessage = errorMessage;
+ endStatus = JobExecutionStatus.ERROR;
+ }
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/log/FlowExecutionLogHandler.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/log/FlowExecutionLogHandler.java
new file mode 100644
index 00000000..6d6f7cf7
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/log/FlowExecutionLogHandler.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.process.runtime.log;
+
+import com.alibaba.fastjson.JSON;
+import com.xiaomi.thain.core.constant.LogLevel;
+import com.xiaomi.thain.core.dao.FlowExecutionDao;
+import com.xiaomi.thain.core.entity.LogEntity;
+import com.xiaomi.thain.core.process.ProcessEngineStorage;
+import lombok.NonNull;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Date 19-5-20 下午5:09
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+public class FlowExecutionLogHandler {
+
+ private final long flowExecutionId;
+ @NonNull
+ private final FlowExecutionDao flowExecutionDao;
+ @NonNull
+ private final List logs;
+
+ private FlowExecutionLogHandler(long flowExecutionId,
+ @NonNull ProcessEngineStorage processEngineStorage) {
+ this.flowExecutionId = flowExecutionId;
+ this.flowExecutionDao = processEngineStorage.flowExecutionDao;
+ logs = new CopyOnWriteArrayList<>();
+
+ }
+
+ public static FlowExecutionLogHandler getInstance(long flowExecutionId,
+ @NonNull ProcessEngineStorage processEngineStorage) {
+ return new FlowExecutionLogHandler(flowExecutionId, processEngineStorage);
+ }
+
+ /**
+ * 插入一个log
+ */
+ public void addInfo(@NonNull String content) {
+ addLog(LogLevel.INFO, content);
+ }
+
+ /**
+ * 结束日志
+ */
+ public void endSuccess() {
+ addInfo("executed successful");
+ }
+
+ public void endError(@NonNull String errorMessage) {
+ addLog(LogLevel.ERROR, errorMessage);
+ }
+
+ private synchronized void addLog(@NonNull LogLevel logLevel, @NonNull String content) {
+ logs.add(LogEntity.builder().level(logLevel.name()).content(content).timestamp(System.currentTimeMillis()).build());
+ flowExecutionDao.updateLogs(flowExecutionId, JSON.toJSONString(logs));
+ }
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/log/JobExecutionLogHandler.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/log/JobExecutionLogHandler.java
new file mode 100644
index 00000000..6ae0160b
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/log/JobExecutionLogHandler.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.process.runtime.log;
+
+import com.alibaba.fastjson.JSON;
+import com.xiaomi.thain.core.constant.LogLevel;
+import com.xiaomi.thain.core.dao.JobExecutionDao;
+import com.xiaomi.thain.core.entity.LogEntity;
+import com.xiaomi.thain.core.process.ProcessEngineStorage;
+import lombok.NonNull;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Date 19-5-20 下午5:09
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+public class JobExecutionLogHandler {
+
+ private final long jobExecutionId;
+ @NonNull
+ private final JobExecutionDao jobExecutionDao;
+
+ @NonNull
+ private final List logs;
+
+ private static final Map JOB_EXECUTION_LOG_HANDLER_MAP = new ConcurrentHashMap<>();
+
+ private JobExecutionLogHandler(long jobExecutionId,
+ @NonNull ProcessEngineStorage processEngineStorage) {
+ this.jobExecutionId = jobExecutionId;
+ this.jobExecutionDao = processEngineStorage.jobExecutionDao;
+ logs = new CopyOnWriteArrayList<>();
+
+ }
+
+ public static JobExecutionLogHandler getInstance(long jobExecutionId,
+ @NonNull ProcessEngineStorage processEngineStorage) {
+ return JOB_EXECUTION_LOG_HANDLER_MAP.computeIfAbsent(jobExecutionId,
+ id -> new JobExecutionLogHandler(jobExecutionId, processEngineStorage));
+ }
+
+ public synchronized void add(@NonNull String content,
+ @NonNull LogLevel logLevel) {
+ logs.add(LogEntity.builder().level(logLevel.name()).content(content).timestamp(System.currentTimeMillis()).build());
+ jobExecutionDao.updateLogs(jobExecutionId, JSON.toJSONString(logs));
+ }
+
+ public void close() {
+ JOB_EXECUTION_LOG_HANDLER_MAP.remove(jobExecutionId);
+ }
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/notice/HttpNotice.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/notice/HttpNotice.java
new file mode 100644
index 00000000..d7340cda
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/notice/HttpNotice.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+
+package com.xiaomi.thain.core.process.runtime.notice;
+
+import com.google.common.collect.ImmutableMap;
+
+import static com.xiaomi.thain.common.constant.HttpCallbackStatus.ERROR;
+import static com.xiaomi.thain.common.constant.HttpCallbackStatus.KILLED;
+import static com.xiaomi.thain.common.constant.HttpCallbackStatus.PAUSE;
+import static com.xiaomi.thain.common.constant.HttpCallbackStatus.START;
+import static com.xiaomi.thain.common.constant.HttpCallbackStatus.SUCCESS;
+
+import com.xiaomi.thain.common.utils.HttpUtils;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Date 19-5-21 上午10:03
+ * http 通知
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+@Slf4j
+public class HttpNotice {
+ @NonNull
+ private final String url;
+ private final long flowId;
+ private final long flowExecutionId;
+
+ private static final String CODE_KEY = "code";
+ private static final String MESSAGE_KEY = "message";
+ private static final String FLOW_ID = "flowId";
+ private static final String FLOW_EXECUTION_ID = "flowExecutionId";
+
+ private HttpNotice(@NonNull String url, long flowId, long flowExecutionId) {
+ this.url = url;
+ this.flowId = flowId;
+ this.flowExecutionId = flowExecutionId;
+ }
+
+ public static HttpNotice getInstance(@NonNull String url, long flowId, long executionId) {
+ return new HttpNotice(url, flowId, executionId);
+ }
+
+ public void sendStart() {
+ checkAndPost(ImmutableMap.of(
+ FLOW_ID, flowId + "",
+ FLOW_EXECUTION_ID, flowExecutionId + "",
+ CODE_KEY, START.code + ""
+ ));
+ }
+
+ public void sendError(@NonNull String errorMessage) {
+ checkAndPost(ImmutableMap.of(
+ FLOW_ID, flowId + "",
+ FLOW_EXECUTION_ID, flowExecutionId + "",
+ CODE_KEY, ERROR.code + "",
+ MESSAGE_KEY, errorMessage
+ ));
+ }
+
+ public void sendKilled() {
+ checkAndPost(ImmutableMap.of(
+ FLOW_ID, flowId + "",
+ FLOW_EXECUTION_ID, flowExecutionId + "",
+ CODE_KEY, KILLED.code + ""));
+ }
+
+ public void sendPause() {
+ checkAndPost(ImmutableMap.of(
+ FLOW_ID, flowId + "",
+ FLOW_EXECUTION_ID, flowExecutionId + "",
+ CODE_KEY, PAUSE.code + ""));
+ }
+
+ public void sendSuccess() {
+ checkAndPost(ImmutableMap.of(
+ FLOW_ID, flowId + "",
+ FLOW_EXECUTION_ID, flowExecutionId + "",
+ CODE_KEY, SUCCESS.code + ""));
+ }
+
+ private void checkAndPost(@NonNull Map data) {
+ if (url.trim().length() == 0) {
+ return;
+ }
+ try {
+ HttpUtils.post(url, data);
+ } catch (IOException e) {
+ log.warn(e.getMessage());
+ }
+ }
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/notice/MailNotice.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/notice/MailNotice.java
new file mode 100644
index 00000000..6659f775
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/notice/MailNotice.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.process.runtime.notice;
+
+import com.xiaomi.thain.core.process.service.MailService;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.logging.log4j.util.Strings;
+
+/**
+ * Date 19-5-21 下午12:46
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+@Slf4j
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public class MailNotice {
+
+ @NonNull
+ private MailService mailService;
+ @NonNull
+ private String callbackEmail;
+
+ public static MailNotice getInstance(@NonNull MailService mailService, @NonNull String callbackEmail) {
+ return new MailNotice(mailService, callbackEmail);
+ }
+
+ /**
+ * 发送错误通知
+ */
+ public void sendError(@NonNull String errorMessage) {
+ if (Strings.isBlank(callbackEmail)) {
+ return;
+ }
+ try {
+ mailService.send(callbackEmail, "Thain flow executed failed", errorMessage);
+ } catch (Exception e) {
+ log.warn(e.getMessage());
+ }
+ }
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/storage/FlowExecutionStorage.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/storage/FlowExecutionStorage.java
new file mode 100644
index 00000000..5bc108a9
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/runtime/storage/FlowExecutionStorage.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+
+package com.xiaomi.thain.core.process.runtime.storage;
+
+import lombok.NonNull;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Date 19-5-17 上午9:43
+ *
+ * 保存流程产生的中间结果
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+public class FlowExecutionStorage {
+
+ private static final Map FLOW_EXECUTION_STORAGE_MAP = new ConcurrentHashMap<>();
+
+ /**
+ * 流程id
+ */
+ private long flowExecutionId;
+
+ @NonNull
+ public final Map storageMap;
+ @NonNull
+ private final Set finishJob;
+
+ private FlowExecutionStorage(long flowExecutionId) {
+ this.flowExecutionId = flowExecutionId;
+ storageMap = new ConcurrentHashMap<>();
+ finishJob = new HashSet<>();
+ }
+
+ public static FlowExecutionStorage getInstance(final long flowExecutionId) {
+ return FLOW_EXECUTION_STORAGE_MAP.computeIfAbsent(flowExecutionId, FlowExecutionStorage::new);
+ }
+
+ public void put(@NonNull final String jobName, @NonNull final String key, @NonNull final Object value) {
+ storageMap.put(jobName + "." + key, value);
+ }
+
+ /**
+ * 添加到完成列表
+ */
+ public void addFinishJob(@NonNull String jobName) {
+ finishJob.add(jobName);
+ }
+
+ /**
+ * 判断Job name 是否finish
+ */
+ public boolean finished(@NonNull String jobName) {
+ return finishJob.contains(jobName);
+ }
+
+ @SuppressWarnings("unchecked")
+ public Optional get(@NonNull final String jobName, @NonNull final String key) {
+ return Optional.ofNullable((T) storageMap.get(jobName + "." + key));
+ }
+
+ public static void drop(@NonNull Long flowExecutionId) {
+ FLOW_EXECUTION_STORAGE_MAP.remove(flowExecutionId);
+ }
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/service/ComponentService.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/service/ComponentService.java
new file mode 100644
index 00000000..e192c422
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/service/ComponentService.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.process.service;
+
+import com.google.common.collect.ImmutableMap;
+import com.xiaomi.thain.component.annotation.ThainComponent;
+import com.xiaomi.thain.core.utils.ReflectUtils;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * @author liangyongrui@xiaomi.com
+ */
+@Slf4j
+public class ComponentService {
+
+ @NonNull
+ private final Map> components;
+ @NonNull
+ private final Map componentJson;
+
+ private ComponentService() {
+ this.components = new HashMap<>();
+ this.componentJson = new HashMap<>();
+ initComponent();
+ }
+
+ public static ComponentService getInstance() {
+ return new ComponentService();
+ }
+
+ /**
+ * 加载Component
+ */
+ private void initComponent() {
+ ReflectUtils.getClassesByAnnotation("com.xiaomi.thain.component", ThainComponent.class)
+ .forEach(t -> {
+ val group = t.getAnnotation(ThainComponent.class).group();
+ val name = t.getAnnotation(ThainComponent.class).name();
+ val defineJson = t.getAnnotation(ThainComponent.class).defineJson();
+ components.put(group + "::" + name, t);
+ componentJson.put(group + "::" + name, defineJson);
+ });
+ }
+
+ /**
+ * 获取组件的定义Map,用于前端展示
+ */
+ public Map getComponentDefineJsonList() {
+ return ImmutableMap.copyOf(componentJson);
+ }
+
+ public Optional> getComponentClass(@NonNull String componentFullName) {
+ return Optional.ofNullable(components.get(componentFullName));
+ }
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/process/service/MailService.java b/thain-core/src/main/java/com/xiaomi/thain/core/process/service/MailService.java
new file mode 100644
index 00000000..f2bf463c
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/process/service/MailService.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.process.service;
+
+import com.xiaomi.thain.core.dao.UserDao;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.activation.DataHandler;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.internet.*;
+import javax.mail.util.ByteArrayDataSource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Date 19-5-21 下午1:24
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+@Slf4j
+public class MailService {
+
+ @NonNull
+ private final String sender;
+ @NonNull
+ private final String senderUsername;
+ @NonNull
+ private final String senderPassword;
+
+ @NonNull
+ private final Properties props;
+
+ @NonNull
+ private final UserDao userDao;
+
+ private MailService(@NonNull String host, @NonNull String sender,
+ @NonNull String senderUsername, @NonNull String senderPassword, @NonNull UserDao userDao) {
+ this.sender = sender;
+ this.senderUsername = senderUsername;
+ this.senderPassword = senderPassword;
+ this.userDao = userDao;
+ props = new Properties();
+ props.setProperty("mail.smtp.auth", "true");
+ props.setProperty("mail.transport.protocol", "smtp");
+ props.setProperty("mail.smtp.host", host);
+ }
+
+ public static MailService getInstance(@NonNull String host, @NonNull String sender, @NonNull String senderUsername,
+ @NonNull String senderPassword, @NonNull UserDao userDao) {
+ return new MailService(host, sender, senderUsername, senderPassword, userDao);
+ }
+
+ /**
+ * 发送邮件
+ *
+ * @param to 邮件发送给to
+ * @param subject 邮件主题
+ * @param content 邮件内容(支持html)
+ * @param attachments 附件
+ * @throws MessagingException if multipart creation failed
+ */
+ public void send(@NonNull String[] to, @NonNull String subject, @NonNull String content, @NonNull Map attachments)
+ throws MessagingException, IOException {
+ Session session = Session.getInstance(props);
+ MimeMessage msg = new MimeMessage(session);
+ msg.setFrom(new InternetAddress(sender));
+ for (String t : to) {
+ msg.addRecipient(Message.RecipientType.TO, new InternetAddress(t));
+ }
+ msg.setSubject(subject, "UTF-8");
+ MimeMultipart mm = new MimeMultipart();
+ MimeBodyPart contentMbp = new MimeBodyPart();
+ contentMbp.setContent(content, "text/html;charset=UTF-8");
+ mm.addBodyPart(contentMbp);
+ if (!attachments.isEmpty()) {
+ for (Map.Entry attachment : attachments.entrySet()) {
+ MimeBodyPart mbp = new MimeBodyPart();
+ ByteArrayDataSource byteArrayDataSource = new ByteArrayDataSource(attachment.getValue(), "application/octet-stream");
+ byteArrayDataSource.setName(attachment.getKey());
+ mbp.setDataHandler(new DataHandler(byteArrayDataSource));
+ mbp.setFileName(MimeUtility.encodeText(attachment.getKey()));
+ mm.addBodyPart(mbp);
+ }
+ }
+ mm.setSubType("mixed");
+ msg.setContent(mm);
+ Transport transport = session.getTransport();
+ transport.connect(senderUsername, senderPassword);
+ transport.sendMessage(msg, msg.getAllRecipients());
+ transport.close();
+ }
+
+ public void send(@NonNull String[] to, @NonNull String subject, @NonNull String content) throws IOException, MessagingException {
+ send(to, subject, content, Collections.emptyMap());
+ }
+
+ public void send(@NonNull String to, @NonNull String subject, @NonNull String content) throws IOException, MessagingException {
+ send(new String[] {to}, subject, content);
+ }
+
+ public void sendSeriousError(@NonNull String s) {
+ try {
+ val emails = userDao.getAdminUsers().stream().map(t -> t.email).filter(StringUtils::isNotBlank).toArray(String[]::new);
+ if (emails.length > 0) {
+ send(emails, "Thain serious error", InetAddress.getLocalHost().toString() + ":\n" + s);
+ }
+ } catch (Exception e) {
+ log.error("", e);
+ }
+ }
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/scheduler/SchedulerEngine.java b/thain-core/src/main/java/com/xiaomi/thain/core/scheduler/SchedulerEngine.java
new file mode 100644
index 00000000..d012803a
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/scheduler/SchedulerEngine.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.scheduler;
+
+import com.xiaomi.thain.common.exception.ThainRuntimeException;
+import com.xiaomi.thain.common.exception.scheduler.ThainSchedulerInitException;
+import com.xiaomi.thain.common.exception.scheduler.ThainSchedulerStartException;
+import com.xiaomi.thain.common.model.FlowModel;
+import com.xiaomi.thain.core.process.ProcessEngine;
+import com.xiaomi.thain.core.scheduler.job.CleanJob;
+import com.xiaomi.thain.core.scheduler.job.FlowJob;
+import com.xiaomi.thain.core.scheduler.job.SlaJob;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.apache.logging.log4j.util.Strings;
+import org.quartz.*;
+import org.quartz.impl.StdSchedulerFactory;
+
+import java.time.Instant;
+import java.util.Date;
+
+import static org.quartz.CronScheduleBuilder.cronSchedule;
+import static org.quartz.JobBuilder.newJob;
+import static org.quartz.TriggerBuilder.newTrigger;
+
+/**
+ * Date 19-5-17 下午1:41
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+@Slf4j
+public class SchedulerEngine {
+
+ @NonNull
+ private final Scheduler scheduler;
+
+ private SchedulerEngine(@NonNull SchedulerEngineConfiguration schedulerEngineConfiguration,
+ @NonNull ProcessEngine processEngine)
+ throws ThainSchedulerInitException {
+ try {
+ val factory = new StdSchedulerFactory();
+ factory.initialize(schedulerEngineConfiguration.properties);
+ this.scheduler = factory.getScheduler();
+ scheduler.setJobFactory((bundle, ignore) -> {
+ try {
+ val method = Class.forName(bundle.getJobDetail().getJobClass().getName())
+ .getMethod("getInstance", ProcessEngine.class);
+ return (Job) method.invoke(null, processEngine);
+ } catch (Exception e) {
+ throw new ThainRuntimeException(e);
+ }
+ });
+
+ initCleanUp();
+ } catch (Exception e) {
+ log.error("thain init failed", e);
+ throw new ThainSchedulerInitException(e.getMessage());
+ }
+ }
+
+ private void initCleanUp() throws SchedulerException {
+ JobDetail jobDetail = newJob(CleanJob.class)
+ .withIdentity("job_clean_up", "system")
+ .build();
+ Trigger trigger = newTrigger()
+ .withIdentity("trigger_clean_up", "system")
+ .withSchedule(cronSchedule("0 0 * * * ?"))
+ .build();
+ scheduler.deleteJob(jobDetail.getKey());
+ scheduler.scheduleJob(jobDetail, trigger);
+ }
+
+ public static SchedulerEngine getInstance(@NonNull SchedulerEngineConfiguration schedulerEngineConfiguration,
+ @NonNull ProcessEngine processEngine)
+ throws ThainSchedulerInitException {
+ return new SchedulerEngine(schedulerEngineConfiguration, processEngine);
+ }
+
+ public void startDelayed(int second) throws ThainSchedulerStartException {
+ try {
+ this.scheduler.startDelayed(second);
+ } catch (SchedulerException e) {
+ log.error("startDelayed, ", e);
+ throw new ThainSchedulerStartException(e.getMessage());
+ }
+ }
+
+ public void addSla(long flowExecutionId, @NonNull FlowModel flowModel) throws SchedulerException {
+ JobDetail jobDetail = newJob(SlaJob.class)
+ .withIdentity("flowExecution_" + flowExecutionId, "flowExecution")
+ .usingJobData("flowExecutionId", flowExecutionId)
+ .usingJobData("flowId", flowModel.id)
+ .build();
+ Trigger trigger = newTrigger()
+ .withIdentity("trigger_" + flowExecutionId, "flowExecution")
+ .startAt(Date.from(Instant.now().plusSeconds(flowModel.slaDuration)))
+ .build();
+ scheduler.deleteJob(jobDetail.getKey());
+ scheduler.scheduleJob(jobDetail, trigger);
+ }
+
+ public void start() throws ThainSchedulerStartException {
+ try {
+ scheduler.start();
+ } catch (SchedulerException e) {
+ log.error("start", e);
+ throw new ThainSchedulerStartException(e.getMessage());
+ }
+ }
+
+ /**
+ * 添加指定任务,加入调度
+ *
+ * @param flowId flow id
+ * @param cron cron
+ */
+ public void addFlow(long flowId, @NonNull String cron) throws SchedulerException {
+ JobDetail jobDetail = newJob(FlowJob.class)
+ .withIdentity("flow_" + flowId, "flow")
+ .usingJobData("flowId", flowId)
+ .build();
+ if (Strings.isBlank(cron)) {
+ scheduler.deleteJob(jobDetail.getKey());
+ return;
+ }
+ Trigger trigger = newTrigger()
+ .withIdentity("trigger_" + flowId, "flow")
+ .withSchedule(cronSchedule(cron))
+ .build();
+ scheduler.deleteJob(jobDetail.getKey());
+ scheduler.scheduleJob(jobDetail, trigger);
+ }
+
+ /**
+ * 删除调度
+ */
+ public void deleteFlow(long flowId) throws SchedulerException {
+ scheduler.deleteJob(new JobKey("flow_" + flowId, "flow"));
+ }
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/scheduler/SchedulerEngineConfiguration.java b/thain-core/src/main/java/com/xiaomi/thain/core/scheduler/SchedulerEngineConfiguration.java
new file mode 100644
index 00000000..eb70016f
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/scheduler/SchedulerEngineConfiguration.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.scheduler;
+
+import lombok.NonNull;
+import lombok.val;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+/**
+ * @author liangyongrui@xiaomi.com
+ * @date 19-5-16 下午8:35
+ */
+public class SchedulerEngineConfiguration {
+
+ @NonNull
+ public final Properties properties;
+
+ private SchedulerEngineConfiguration(@NonNull Properties properties) {
+ this.properties = properties;
+ }
+
+ public static SchedulerEngineConfiguration getInstanceByInputStream(@NonNull InputStream in) throws IOException {
+ val properties = new Properties();
+ properties.load(in);
+ return new SchedulerEngineConfiguration(properties);
+ }
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/scheduler/job/CleanJob.java b/thain-core/src/main/java/com/xiaomi/thain/core/scheduler/job/CleanJob.java
new file mode 100644
index 00000000..c15d94e0
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/scheduler/job/CleanJob.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+
+package com.xiaomi.thain.core.scheduler.job;
+
+import com.xiaomi.thain.core.process.ProcessEngine;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author liangyongrui
+ */
+@Slf4j
+public class CleanJob implements Job {
+
+ @NonNull
+ private ProcessEngine processEngine;
+
+ private static final Map CLEAN_JOB_MAP = new ConcurrentHashMap<>();
+
+ private CleanJob(@NonNull ProcessEngine processEngine) {
+ this.processEngine = processEngine;
+ }
+
+ public static CleanJob getInstance(@NonNull ProcessEngine processEngine) {
+ return CLEAN_JOB_MAP.computeIfAbsent(processEngine.processEngineId, t -> new CleanJob(processEngine));
+ }
+
+ @Override
+ public void execute(@NonNull JobExecutionContext context) {
+ val flowExecutionDao = processEngine.processEngineStorage.flowExecutionDao;
+ flowExecutionDao.cleanFlowExecution();
+ val flowIds = processEngine.processEngineStorage.flowDao.getAllFlowIds();
+ val needDeleteFlowExecutionIds = flowExecutionDao.getNeedDeleteFlowExecutionId(flowIds);
+ flowExecutionDao.deleteFlowExecutionByIds(needDeleteFlowExecutionIds);
+
+ val flowExecutionIds = flowExecutionDao.getAllFlowExecutionIds();
+ val jobExecutionDao = processEngine.processEngineStorage.jobExecutionDao;
+ val needDeleteJobExecutionIds = jobExecutionDao.getNeedDeleteJobExecutionIds(flowExecutionIds);
+ jobExecutionDao.deleteJobExecutionByIds(needDeleteJobExecutionIds);
+ }
+
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/scheduler/job/FlowJob.java b/thain-core/src/main/java/com/xiaomi/thain/core/scheduler/job/FlowJob.java
new file mode 100644
index 00000000..33f1af87
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/scheduler/job/FlowJob.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+
+package com.xiaomi.thain.core.scheduler.job;
+
+import com.xiaomi.thain.common.exception.ThainException;
+import com.xiaomi.thain.common.exception.ThainFlowRunningException;
+import com.xiaomi.thain.core.process.ProcessEngine;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author liangyongrui
+ */
+@Slf4j
+public class FlowJob implements Job {
+
+ @NonNull
+ private ProcessEngine processEngine;
+
+ private static final Map FLOW_JOB_MAP = new ConcurrentHashMap<>();
+
+ private FlowJob(@NonNull ProcessEngine processEngine) {
+ this.processEngine = processEngine;
+ }
+
+ public static FlowJob getInstance(@NonNull ProcessEngine processEngine) {
+ return FLOW_JOB_MAP.computeIfAbsent(processEngine.processEngineId,
+ t -> new FlowJob(processEngine));
+ }
+
+ @Override
+ public void execute(@NonNull JobExecutionContext context) {
+ try {
+ long flowId = context.getJobDetail().getJobDataMap().getLong("flowId");
+ log.info("auto execution: " + flowId);
+ processEngine.schedulerStartProcess(flowId);
+ } catch (ThainFlowRunningException e) {
+ log.warn(ExceptionUtils.getRootCauseMessage(e));
+ } catch (ThainException e) {
+ log.error("Failed to auto trigger flow:", e);
+ }
+ }
+
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/scheduler/job/SlaJob.java b/thain-core/src/main/java/com/xiaomi/thain/core/scheduler/job/SlaJob.java
new file mode 100644
index 00000000..febbc3a7
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/scheduler/job/SlaJob.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.scheduler.job;
+
+import com.xiaomi.thain.common.constant.FlowExecutionStatus;
+import com.xiaomi.thain.common.exception.ThainRuntimeException;
+import com.xiaomi.thain.core.process.ProcessEngine;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.apache.logging.log4j.util.Strings;
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Date 19-7-16 上午10:59
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+@Slf4j
+public class SlaJob implements Job {
+
+ @NonNull
+ private ProcessEngine processEngine;
+
+ private static final Map SLA_JOB_MAP = new ConcurrentHashMap<>();
+
+ private SlaJob(@NonNull ProcessEngine processEngine) {
+ this.processEngine = processEngine;
+ }
+
+ public static SlaJob getInstance(@NonNull ProcessEngine processEngine) {
+ return SLA_JOB_MAP.computeIfAbsent(processEngine.processEngineId, t -> new SlaJob(processEngine));
+ }
+
+ @Override
+ public void execute(@NonNull JobExecutionContext context) {
+ val dataMap = context.getJobDetail().getJobDataMap();
+ long flowId = dataMap.getLong("flowId");
+ long flowExecutionId = dataMap.getLong("flowExecutionId");
+ val flowExecutionModel = processEngine.processEngineStorage.flowExecutionDao
+ .getFlowExecution(flowExecutionId)
+ .orElseThrow(() -> new ThainRuntimeException("flowExecution id does not exist:" + flowExecutionId));
+
+ if (flowExecutionModel.status == FlowExecutionStatus.RUNNING.code) {
+ try {
+ val flow = processEngine.processEngineStorage.flowDao.getFlow(flowId)
+ .orElseThrow(() -> new ThainRuntimeException("flow does not exist, flowId:" + flowId));
+ if (flow.slaKill) {
+ processEngine.thainFacade.killFlowExecution(flowExecutionId);
+ }
+ if (Strings.isNotBlank(flow.slaEmail)) {
+ processEngine.processEngineStorage.mailService.send(
+ flow.slaEmail.trim().split(","),
+ "Thain SLA提醒",
+ "您的任务:" + flow.name + "(" + flow.id + "), 超出期望的执行时间"
+ );
+ }
+ } catch (Exception e) {
+ log.error("kill failed, flowExecutionId:" + flowExecutionId, e);
+ }
+ }
+ }
+
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/thread/pool/ThainThreadPool.java b/thain-core/src/main/java/com/xiaomi/thain/core/thread/pool/ThainThreadPool.java
new file mode 100644
index 00000000..7f5f67de
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/thread/pool/ThainThreadPool.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+package com.xiaomi.thain.core.thread.pool;
+
+import lombok.NonNull;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Nonnull;
+
+/**
+ * Date 19-5-17 下午7:47
+ * 线程池
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+public class ThainThreadPool implements Executor {
+
+ @NonNull
+ private final ThreadPoolExecutor threadPoolExecutor;
+
+ private ThainThreadPool(@NonNull String threadName, int corePoolSize, int maximumPoolSize, long keepAliveSecond) {
+ threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveSecond,
+ TimeUnit.SECONDS, new LinkedBlockingDeque<>(), runnable -> new Thread(runnable, threadName));
+ }
+
+ public static ThainThreadPool getInstance(@NonNull String threadName, int corePoolSize, int maximumPoolSize, long keepAliveSecond) {
+ return new ThainThreadPool(threadName, corePoolSize, maximumPoolSize, keepAliveSecond);
+ }
+
+ @Override
+ public void execute(@Nonnull Runnable command) {
+ threadPoolExecutor.execute(command);
+ }
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/utils/H2Extended.java b/thain-core/src/main/java/com/xiaomi/thain/core/utils/H2Extended.java
new file mode 100644
index 00000000..9c590186
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/utils/H2Extended.java
@@ -0,0 +1,12 @@
+package com.xiaomi.thain.core.utils;
+
+import lombok.NonNull;
+
+/**
+ * @author liangyongrui
+ */
+public class H2Extended {
+ public static int unixTimestamp(@NonNull java.sql.Timestamp timestamp) {
+ return (int) (timestamp.getTime() / 1000L);
+ }
+}
\ No newline at end of file
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/utils/ReflectUtils.java b/thain-core/src/main/java/com/xiaomi/thain/core/utils/ReflectUtils.java
new file mode 100644
index 00000000..0c241197
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/utils/ReflectUtils.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+
+package com.xiaomi.thain.core.utils;
+
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.annotation.Annotation;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.util.*;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.stream.Collectors;
+
+/**
+ * 反射相关操作
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+@Slf4j
+public class ReflectUtils {
+
+ private ReflectUtils() {
+ }
+
+ public static Collection> getClassesByAnnotation(@NonNull final String basePackage,
+ @NonNull final Class extends Annotation> annotation) {
+ return getClasses(basePackage).stream()
+ .filter(t -> t.isAnnotationPresent(annotation))
+ .collect(Collectors.toList());
+ }
+
+ private static Set> scanFile(URL url, String packageName)
+ throws UnsupportedEncodingException, ClassNotFoundException {
+ log.info("scanning of file type");
+ String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
+ return findAndAddClassesInPackageByFile(packageName, filePath);
+ }
+
+ private static Set> scanJar(URL url, String packageName, String packageDirName)
+ throws IOException, ClassNotFoundException {
+ val result = new HashSet>();
+ log.info("scanning of jar type");
+ JarFile jar = ((JarURLConnection) url.openConnection()).getJarFile();
+ Enumeration entries = jar.entries();
+ while (entries.hasMoreElements()) {
+ JarEntry entry = entries.nextElement();
+ String name = entry.getName();
+ if (name.charAt(0) == '/') {
+ name = name.substring(1);
+ }
+ if (!name.startsWith(packageDirName)) {
+ continue;
+ }
+ int idx = name.lastIndexOf('/');
+ if (idx != -1) {
+ packageName = name.substring(0, idx).replace('/', '.');
+ }
+ if (name.endsWith(".class") && !entry.isDirectory()) {
+ String className = name.substring(packageName.length() + 1, name.length() - 6);
+ result.add(Class.forName(packageName + '.' + className));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * 从包package中获取所有的Class
+ */
+ private static Set> getClasses(String packageName) {
+
+ val classes = new HashSet>();
+ String packageDirName = packageName.replace('.', '/');
+ try {
+ Enumeration dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
+ while (dirs.hasMoreElements()) {
+ URL url = dirs.nextElement();
+ String protocol = url.getProtocol();
+ if ("file".equals(protocol)) {
+ classes.addAll(scanFile(url, packageName));
+ } else if ("jar".equals(protocol)) {
+ classes.addAll(scanJar(url, packageName, packageDirName));
+ }
+ }
+ } catch (Exception e) {
+ log.error("", e);
+ }
+
+ return classes;
+ }
+
+ /**
+ * 以文件的形式来获取包下的所有Class
+ */
+ private static Set> findAndAddClassesInPackageByFile(String packageName, String packagePath) throws ClassNotFoundException {
+ val dir = new File(packagePath);
+ if (!dir.exists() || !dir.isDirectory()) {
+ log.warn("There is nothing in {} which user defined", packageName);
+ return Collections.emptySet();
+ }
+ val dirFiles = dir.listFiles(file -> file.isDirectory() || (file.getName().endsWith(".class")));
+ if (Objects.isNull(dirFiles)) {
+ return Collections.emptySet();
+ }
+ val result = new HashSet>();
+ for (val file : dirFiles) {
+ if (file.isDirectory()) {
+ result.addAll(findAndAddClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath()));
+ } else {
+ String className = file.getName().substring(0, file.getName().length() - 6);
+ Class> aClass = Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className);
+ result.add(aClass);
+ }
+ }
+ return result;
+ }
+
+}
diff --git a/thain-core/src/main/java/com/xiaomi/thain/core/utils/SendModifyUtils.java b/thain-core/src/main/java/com/xiaomi/thain/core/utils/SendModifyUtils.java
new file mode 100644
index 00000000..4f089f54
--- /dev/null
+++ b/thain-core/src/main/java/com/xiaomi/thain/core/utils/SendModifyUtils.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+
+package com.xiaomi.thain.core.utils;
+
+import com.google.common.collect.ImmutableMap;
+import com.xiaomi.thain.common.utils.HttpUtils;
+import lombok.NonNull;
+
+import java.io.IOException;
+
+/**
+ * Date 19-7-10 下午2:07
+ *
+ * @author liangyongrui@xiaomi.com
+ */
+public class SendModifyUtils {
+
+ private static final int PAUSE = 1;
+ private static final int SCHEDULING = 2;
+
+ private SendModifyUtils() {
+
+ }
+
+ public static void sendPause(long flowId, @NonNull String modifyCallbackUrl) throws IOException {
+ HttpUtils.post(modifyCallbackUrl, ImmutableMap.of("flowId", flowId, "status", PAUSE));
+ }
+
+ public static void sendScheduling(long flowId, @NonNull String modifyCallbackUrl) throws IOException {
+ HttpUtils.post(modifyCallbackUrl, ImmutableMap.of("flowId", flowId, "status", SCHEDULING));
+ }
+}
diff --git a/thain-core/src/main/resources/com/xiaomi/thain/core/mapper/FlowExecutionMapper.xml b/thain-core/src/main/resources/com/xiaomi/thain/core/mapper/FlowExecutionMapper.xml
new file mode 100644
index 00000000..b0e21e6c
--- /dev/null
+++ b/thain-core/src/main/resources/com/xiaomi/thain/core/mapper/FlowExecutionMapper.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+ insert into thain_flow_execution (flow_id, `status`, host_info, trigger_type, `logs`, create_time, update_time)
+ values (#{flowId}, #{status}, #{hostInfo}, #{triggerType}, #{logs}, now(), now())
+
+
+
+ update thain_flow_execution
+ set logs = #{content}
+ where id = #{flowExecutionId}
+
+
+ update thain_flow_execution
+ set status = #{status}
+ where id = #{flowExecutionId}
+
+
+ update thain_flow_execution
+ set status = 4
+ where id = #{flowExecutionId}
+
+
+ delete
+ from thain_flow_execution
+ where (unix_timestamp(now()) - unix_timestamp(update_time)) > #{dataReserveDays} * 24 * 60 * 60
+
+
+ delete
+ from thain_flow_execution
+ where id in (
+
+ #{id}
+
+ )
+
+
+
+
+
+
+
diff --git a/thain-core/src/main/resources/com/xiaomi/thain/core/mapper/FlowMapper.xml b/thain-core/src/main/resources/com/xiaomi/thain/core/mapper/FlowMapper.xml
new file mode 100644
index 00000000..8df1092e
--- /dev/null
+++ b/thain-core/src/main/resources/com/xiaomi/thain/core/mapper/FlowMapper.xml
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+ insert into thain_flow (name,
+
+ cron,
+
+ create_user,
+
+ callback_url,
+
+ pause_continuous_failure,
+
+ email_continuous_failure,
+
+
+ modify_callback_url,
+
+
+ callback_email,
+
+
+ create_app_id,
+
+ sla_duration,
+
+ sla_email,
+
+ sla_kill,
+
+ last_run_status,
+
+ scheduling_status,
+ create_time, update_time, status_update_time)
+ values (#{name},
+
+ #{cron},
+
+ #{createUser},
+
+ #{callbackUrl},
+
+ #{pauseContinuousFailure},
+
+ #{emailContinuousFailure},
+
+
+ #{modifyCallbackUrl},
+
+
+ #{callbackEmail},
+
+
+ #{createAppId},
+
+ #{slaDuration},
+
+ #{slaEmail},
+
+ #{slaKill},
+
+ #{lastRunStatus},
+
+ #{schedulingStatus},
+ now(), now(), now())
+
+
+ insert into thain_job(
+ flow_id,
+ name,
+ `condition`,
+ callback_url,
+ properties,
+ x_axis,
+ y_axis,
+ component
+ ) values
+
+ (
+ #{job.flowId},
+ #{job.name},
+
+ #{job.condition}
+ ''
+
+ ,
+
+ #{job.callbackUrl}
+ ''
+
+ ,
+
+ #{job.propertiesString}
+ ''
+
+ ,
+
+ #{job.xAxis}
+ 0
+
+ ,
+
+ #{job.yAxis}
+ 0
+
+ ,
+ #{job.component}
+ )
+
+
+
+
+ update thain_flow
+ set `id`= #{id},
+
+ `name` = #{name},
+
+
+ `cron` = #{cron},
+
+
+ modify_callback_url = #{modifyCallbackUrl},
+
+
+ pause_continuous_failure = #{pauseContinuousFailure},
+
+
+ email_continuous_failure = #{emailContinuousFailure},
+
+
+ `callback_url` = #{callbackUrl},
+
+
+ `callback_email` = #{callbackEmail},
+
+
+ `sla_duration` = #{slaDuration},
+
+
+ `sla_email` = #{slaEmail},
+
+
+ `sla_kill` = #{slaKill},
+
+ `update_time` = now(),
+ `scheduling_status` = #{schedulingStatus}
+ where id = #{id}
+
+
+
+ update thain_flow
+ set last_run_status = #{lastRunStatus}
+ where id = #{flowId}
+
+
+
+ delete
+ from thain_flow
+ where id = #{flowId}
+
+
+ delete
+ from thain_job
+ where flow_id = #{flowId}
+
+
+ update thain_job
+ set deleted = 1
+ where flow_id = #{flowId}
+
+
+ update thain_flow
+ set scheduling_status = #{schedulingStatus},
+ update_time = now()
+ where id = #{flowId}
+
+
+
+
+
diff --git a/thain-core/src/main/resources/com/xiaomi/thain/core/mapper/JobExecutionMapper.xml b/thain-core/src/main/resources/com/xiaomi/thain/core/mapper/JobExecutionMapper.xml
new file mode 100644
index 00000000..dc7d8d85
--- /dev/null
+++ b/thain-core/src/main/resources/com/xiaomi/thain/core/mapper/JobExecutionMapper.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+ insert into thain_job_execution (flow_execution_id, job_id, status, logs, create_time, update_time)
+ values (#{flowExecutionId}, #{jobId}, #{status}, #{logs}, now(), now())
+
+
+
+ update thain_job_execution
+ set logs = #{logs}
+ where id = #{jobExecutionId}
+
+
+ update thain_job_execution
+ set status = #{status}
+ where id = #{jobExecutionId}
+
+
+ update thain_job_execution
+ set create_time = now()
+ where id = #{jobExecutionId}
+
+
+ delete
+ from thain_job_execution
+ where id in (
+
+ #{id}
+
+ )
+
+
+
diff --git a/thain-core/src/main/resources/com/xiaomi/thain/core/mapper/JobMapper.xml b/thain-core/src/main/resources/com/xiaomi/thain/core/mapper/JobMapper.xml
new file mode 100644
index 00000000..98a89e9a
--- /dev/null
+++ b/thain-core/src/main/resources/com/xiaomi/thain/core/mapper/JobMapper.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
diff --git a/thain-core/src/main/resources/com/xiaomi/thain/core/mapper/UserMapper.xml b/thain-core/src/main/resources/com/xiaomi/thain/core/mapper/UserMapper.xml
new file mode 100644
index 00000000..b4ec8406
--- /dev/null
+++ b/thain-core/src/main/resources/com/xiaomi/thain/core/mapper/UserMapper.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/thain-core/src/main/resources/schema/componentJsonSchema.json b/thain-core/src/main/resources/schema/componentJsonSchema.json
new file mode 100644
index 00000000..59525ed1
--- /dev/null
+++ b/thain-core/src/main/resources/schema/componentJsonSchema.json
@@ -0,0 +1,94 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "ComponentDefineJson": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "property": {
+ "description": "传到后端的key",
+ "type": "string",
+ "title": "property"
+ },
+ "required": {
+ "description": "是否必填,false 或不写 则为不是必填",
+ "type": "boolean",
+ "title": "required"
+ },
+ "label": {
+ "description": "输入框前面的标识,不写默认用property",
+ "type": "string",
+ "title": "label"
+ },
+ "input": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "id": {
+ "enum": [
+ "line",
+ "richText",
+ "shell",
+ "sql",
+ "textarea"
+ ],
+ "type": "string",
+ "title": "id"
+ }
+ },
+ "required": [
+ "id"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "enum": [
+ "select"
+ ],
+ "title": "id"
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "属性的值",
+ "type": "string",
+ "title": "id"
+ },
+ "name": {
+ "description": "下拉框中的候选项,不写用id代替",
+ "type": "string",
+ "title": "name"
+ }
+ },
+ "required": [
+ "id"
+ ]
+ },
+ "title": "options"
+ }
+ },
+ "required": [
+ "id",
+ "options"
+ ]
+ }
+ ],
+ "title": "input"
+ }
+ },
+ "required": [
+ "input",
+ "property"
+ ]
+ }
+ }
+ }
+}
diff --git a/thain-core/src/main/resources/sql/h2/init_data.sql b/thain-core/src/main/resources/sql/h2/init_data.sql
new file mode 100644
index 00000000..08763cea
--- /dev/null
+++ b/thain-core/src/main/resources/sql/h2/init_data.sql
@@ -0,0 +1,2 @@
+INSERT INTO thain_user(user_id, user_name, password_hash, admin)
+VALUES ('admin', 'admin', '$2a$10$hm.fwmx.bnV5wMPSWybqGeTj0wBTCrhGPub1dPChRVwCJtT.2y8WG', 1);
\ No newline at end of file
diff --git a/thain-core/src/main/resources/sql/h2/quartz.sql b/thain-core/src/main/resources/sql/h2/quartz.sql
new file mode 100644
index 00000000..3b4daf4f
--- /dev/null
+++ b/thain-core/src/main/resources/sql/h2/quartz.sql
@@ -0,0 +1,147 @@
+-- Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+-- This source code is licensed under the Apache License Version 2.0, which
+-- can be found in the LICENSE file in the root directory of this source tree.
+
+CREATE TABLE QRTZ_JOB_DETAILS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ JOB_NAME VARCHAR(100) NOT NULL,
+ JOB_GROUP VARCHAR(100) NOT NULL,
+ DESCRIPTION VARCHAR(250) NULL,
+ JOB_CLASS_NAME VARCHAR(250) NOT NULL,
+ IS_DURABLE VARCHAR(100) NOT NULL,
+ IS_NONCONCURRENT VARCHAR(100) NOT NULL,
+ IS_UPDATE_DATA VARCHAR(100) NOT NULL,
+ REQUESTS_RECOVERY VARCHAR(100) NOT NULL,
+ JOB_DATA BLOB NULL,
+ PRIMARY KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
+);
+
+CREATE TABLE QRTZ_TRIGGERS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_GROUP VARCHAR(100) NOT NULL,
+ JOB_NAME VARCHAR(100) NOT NULL,
+ JOB_GROUP VARCHAR(100) NOT NULL,
+ DESCRIPTION VARCHAR(250) NULL,
+ NEXT_FIRE_TIME BIGINT(13) NULL,
+ PREV_FIRE_TIME BIGINT(13) NULL,
+ PRIORITY INTEGER NULL,
+ TRIGGER_STATE VARCHAR(16) NOT NULL,
+ TRIGGER_TYPE VARCHAR(8) NOT NULL,
+ START_TIME BIGINT(13) NOT NULL,
+ END_TIME BIGINT(13) NULL,
+ CALENDAR_NAME VARCHAR(200) NULL,
+ MISFIRE_INSTR SMALLINT(2) NULL,
+ JOB_DATA BLOB NULL,
+ PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
+ FOREIGN KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
+ REFERENCES QRTZ_JOB_DETAILS (SCHED_NAME, JOB_NAME, JOB_GROUP)
+);
+
+CREATE TABLE QRTZ_SIMPLE_TRIGGERS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_GROUP VARCHAR(100) NOT NULL,
+ REPEAT_COUNT BIGINT(7) NOT NULL,
+ REPEAT_INTERVAL BIGINT(12) NOT NULL,
+ TIMES_TRIGGERED BIGINT(10) NOT NULL,
+ PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
+ FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
+ REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
+);
+
+CREATE TABLE QRTZ_CRON_TRIGGERS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_GROUP VARCHAR(100) NOT NULL,
+ CRON_EXPRESSION VARCHAR(200) NOT NULL,
+ TIME_ZONE_ID VARCHAR(80),
+ PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
+ FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
+ REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
+);
+
+CREATE TABLE QRTZ_SIMPROP_TRIGGERS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_GROUP VARCHAR(100) NOT NULL,
+ STR_PROP_1 VARCHAR(512) NULL,
+ STR_PROP_2 VARCHAR(512) NULL,
+ STR_PROP_3 VARCHAR(512) NULL,
+ INT_PROP_1 INT NULL,
+ INT_PROP_2 INT NULL,
+ LONG_PROP_1 BIGINT NULL,
+ LONG_PROP_2 BIGINT NULL,
+ DEC_PROP_1 NUMERIC(13, 4) NULL,
+ DEC_PROP_2 NUMERIC(13, 4) NULL,
+ BOOL_PROP_1 VARCHAR(100) NULL,
+ BOOL_PROP_2 VARCHAR(100) NULL,
+ PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
+ FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
+ REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
+);
+
+CREATE TABLE QRTZ_BLOB_TRIGGERS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_GROUP VARCHAR(100) NOT NULL,
+ BLOB_DATA BLOB NULL,
+ PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
+ FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
+ REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
+);
+
+CREATE TABLE QRTZ_CALENDARS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ CALENDAR_NAME VARCHAR(100) NOT NULL,
+ CALENDAR BLOB NOT NULL,
+ PRIMARY KEY (SCHED_NAME, CALENDAR_NAME)
+);
+
+CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_GROUP VARCHAR(100) NOT NULL,
+ PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP)
+);
+
+CREATE TABLE QRTZ_FIRED_TRIGGERS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ ENTRY_ID VARCHAR(95) NOT NULL,
+ TRIGGER_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_GROUP VARCHAR(100) NOT NULL,
+ INSTANCE_NAME VARCHAR(100) NOT NULL,
+ FIRED_TIME BIGINT(13) NOT NULL,
+ SCHED_TIME BIGINT(13) NOT NULL,
+ PRIORITY INTEGER NOT NULL,
+ STATE VARCHAR(16) NOT NULL,
+ JOB_NAME VARCHAR(100) NULL,
+ JOB_GROUP VARCHAR(100) NULL,
+ IS_NONCONCURRENT VARCHAR(100) NULL,
+ REQUESTS_RECOVERY VARCHAR(100) NULL,
+ PRIMARY KEY (SCHED_NAME, ENTRY_ID)
+);
+
+CREATE TABLE QRTZ_SCHEDULER_STATE
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ INSTANCE_NAME VARCHAR(100) NOT NULL,
+ LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
+ CHECKIN_INTERVAL BIGINT(13) NOT NULL,
+ PRIMARY KEY (SCHED_NAME, INSTANCE_NAME)
+);
+
+CREATE TABLE QRTZ_LOCKS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ LOCK_NAME VARCHAR(40) NOT NULL,
+ PRIMARY KEY (SCHED_NAME, LOCK_NAME)
+);
diff --git a/thain-core/src/main/resources/sql/h2/spring_session.sql b/thain-core/src/main/resources/sql/h2/spring_session.sql
new file mode 100644
index 00000000..c8803e6a
--- /dev/null
+++ b/thain-core/src/main/resources/sql/h2/spring_session.sql
@@ -0,0 +1,24 @@
+CREATE TABLE SPRING_SESSION
+(
+ PRIMARY_ID CHAR(36) NOT NULL,
+ SESSION_ID CHAR(36) NOT NULL,
+ CREATION_TIME BIGINT NOT NULL,
+ LAST_ACCESS_TIME BIGINT NOT NULL,
+ MAX_INACTIVE_INTERVAL INT NOT NULL,
+ EXPIRY_TIME BIGINT NOT NULL,
+ PRINCIPAL_NAME VARCHAR(100),
+ CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
+);
+
+CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
+CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME);
+CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);
+
+CREATE TABLE SPRING_SESSION_ATTRIBUTES
+(
+ SESSION_PRIMARY_ID CHAR(36) NOT NULL,
+ ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
+ ATTRIBUTE_BYTES LONGVARBINARY NOT NULL,
+ CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
+ CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION (PRIMARY_ID) ON DELETE CASCADE
+);
\ No newline at end of file
diff --git a/thain-core/src/main/resources/sql/h2/thain.sql b/thain-core/src/main/resources/sql/h2/thain.sql
new file mode 100644
index 00000000..2b80bad5
--- /dev/null
+++ b/thain-core/src/main/resources/sql/h2/thain.sql
@@ -0,0 +1,95 @@
+-- Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+-- This source code is licensed under the Apache License Version 2.0, which
+-- can be found in the LICENSE file in the root directory of this source tree.
+CREATE
+ALIAS if not exists UNIX_TIMESTAMP FOR "com.xiaomi.thain.core.utils.H2Extended.unixTimestamp";
+
+create table thain_flow
+(
+ id int auto_increment primary key,
+ name varchar(100) default '' not null comment 'Flow名称,显示用',
+ cron varchar(100) default '' not null comment 'cron 表达式',
+ modify_callback_url varchar(512) default '' not null comment '修改回调地址',
+ pause_continuous_failure int default 0 not null comment '连续失败导致暂停的次数,0表示不暂停',
+ email_continuous_failure varchar(512) default '' not null comment '连续失败pause_continuous_failures次数后,发送邮件的邮箱',
+ create_user varchar(100) default '' not null comment '创建人',
+ callback_url varchar(512) default '' not null comment '状态回调地址',
+ callback_email varchar(512) default '' not null comment '状态回调邮箱',
+ create_app_id varchar(128) default '' not null comment '创建app id, 0为网页上创建',
+ sla_duration int default 0 not null comment '期待执行的时间(秒)',
+ sla_email varchar(512) default '' not null comment '超过期望时间发通知的收件人,多个用逗号隔开',
+ sla_kill int(1) default 0 not null comment '超过期望时间 是否kill',
+ last_run_status int default 0 not null comment '最后一次运行状态:1 未运行、2 运行成功、3 运行异常、4 正在运行、5 手动杀死、6 暂停运行(运行了一半,点了暂停)',
+ scheduling_status int default 0 not null comment '调度状态:1 未设置调度,2 调度中,3 暂停调度',
+ create_time timestamp default '2019-01-01 00:00:00' not null comment '创建时间',
+ update_time timestamp default '2019-01-01 00:00:00' not null comment '更新时间',
+ status_update_time timestamp default '2019-01-01 00:00:00' not null comment '状态更新时间',
+ deleted int(1) default 0 not null comment '标记是否删除'
+);
+
+
+
+create table thain_flow_execution
+(
+ id int auto_increment primary key,
+ flow_id int default 0 not null comment '所属flow id',
+ status int default 1 not null comment '流程执行状态,1 执行中,2 执行结束,3执行异常,4 手动kill',
+ host_info varchar(128) default '' not null comment '机器信息',
+ trigger_type int default 1 not null comment '触发类型 1手动 2自动',
+ logs mediumtext null comment '日志',
+ create_time timestamp default '2019-01-01 00:00:00' not null comment '创建时间',
+ update_time timestamp default '2019-01-01 00:00:00' not null comment '更新时间'
+);
+
+create table thain_job
+(
+ id int auto_increment primary key,
+ flow_id int default 0 not null,
+ name varchar(128) default '0' not null comment 'flow id 对应的name 不能重复',
+ `condition` varchar(256) default '' not null comment '触发条件',
+ component varchar(128) default '' not null comment 'job 所用组件名称',
+ callback_url text null comment '状态回调地址',
+ properties text not null comment '组件属性,json表示',
+ x_axis int default 0 not null comment '横坐标',
+ y_axis int default 0 not null comment '纵坐标',
+ create_time timestamp default CURRENT_TIMESTAMP not null comment 'create time',
+ deleted int(1) default 0 not null comment 'deleted'
+);
+
+create table thain_job_execution
+(
+ id int auto_increment primary key,
+ flow_execution_id int default 0 not null comment '关联的flow_execution',
+ job_id int default 0 not null comment 'job id',
+ status int default 0 not null comment '节点执行状态:1未执行,2执行中,3执行结束,4执行异常',
+ logs mediumtext null comment 'job running logs',
+ create_time timestamp default '2019-01-01 00:00:00' not null comment 'create time',
+ update_time timestamp default '2019-01-01 00:00:00' not null comment 'update time'
+);
+
+
+create table thain_user
+(
+ id int auto_increment primary key,
+ user_id varchar(100) default '' not null comment '用户id',
+ user_name varchar(100) default '' not null comment '用户名',
+ password_hash varchar(100) default '' not null comment '密码',
+ email varchar(300) default '' not null comment 'email',
+ admin int(1) default 0 not null comment '是否管理员',
+ constraint thain_user_user_id_uindex
+ unique (user_id)
+);
+
+create table thain_x5_config
+(
+ id int auto_increment primary key,
+ app_id varchar(128) default '' not null comment 'app id',
+ app_key varchar(128) default '' not null comment 'app key',
+ app_name varchar(128) default '' not null comment 'app名称',
+ principal varchar(1024) default '' not null comment '负责人',
+ app_description varchar(512) default '' not null comment '描述',
+ create_time timestamp default CURRENT_TIMESTAMP not null comment 'create time',
+ constraint sch_x5_config_app_id_uindex
+ unique (app_id)
+);
+
diff --git a/thain-core/src/main/resources/sql/mysql/init_data.sql b/thain-core/src/main/resources/sql/mysql/init_data.sql
new file mode 100644
index 00000000..08763cea
--- /dev/null
+++ b/thain-core/src/main/resources/sql/mysql/init_data.sql
@@ -0,0 +1,2 @@
+INSERT INTO thain_user(user_id, user_name, password_hash, admin)
+VALUES ('admin', 'admin', '$2a$10$hm.fwmx.bnV5wMPSWybqGeTj0wBTCrhGPub1dPChRVwCJtT.2y8WG', 1);
\ No newline at end of file
diff --git a/thain-core/src/main/resources/sql/mysql/quartz.sql b/thain-core/src/main/resources/sql/mysql/quartz.sql
new file mode 100644
index 00000000..2fd43212
--- /dev/null
+++ b/thain-core/src/main/resources/sql/mysql/quartz.sql
@@ -0,0 +1,147 @@
+-- Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+-- This source code is licensed under the Apache License Version 2.0, which
+-- can be found in the LICENSE file in the root directory of this source tree.
+
+CREATE TABLE QRTZ_JOB_DETAILS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ JOB_NAME VARCHAR(100) NOT NULL,
+ JOB_GROUP VARCHAR(100) NOT NULL,
+ DESCRIPTION VARCHAR(250) NULL,
+ JOB_CLASS_NAME VARCHAR(250) NOT NULL,
+ IS_DURABLE VARCHAR(1) NOT NULL,
+ IS_NONCONCURRENT VARCHAR(1) NOT NULL,
+ IS_UPDATE_DATA VARCHAR(1) NOT NULL,
+ REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
+ JOB_DATA BLOB NULL,
+ PRIMARY KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
+);
+
+CREATE TABLE QRTZ_TRIGGERS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_GROUP VARCHAR(100) NOT NULL,
+ JOB_NAME VARCHAR(100) NOT NULL,
+ JOB_GROUP VARCHAR(100) NOT NULL,
+ DESCRIPTION VARCHAR(250) NULL,
+ NEXT_FIRE_TIME BIGINT(13) NULL,
+ PREV_FIRE_TIME BIGINT(13) NULL,
+ PRIORITY INTEGER NULL,
+ TRIGGER_STATE VARCHAR(16) NOT NULL,
+ TRIGGER_TYPE VARCHAR(8) NOT NULL,
+ START_TIME BIGINT(13) NOT NULL,
+ END_TIME BIGINT(13) NULL,
+ CALENDAR_NAME VARCHAR(200) NULL,
+ MISFIRE_INSTR SMALLINT(2) NULL,
+ JOB_DATA BLOB NULL,
+ PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
+ FOREIGN KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
+ REFERENCES QRTZ_JOB_DETAILS (SCHED_NAME, JOB_NAME, JOB_GROUP)
+);
+
+CREATE TABLE QRTZ_SIMPLE_TRIGGERS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_GROUP VARCHAR(100) NOT NULL,
+ REPEAT_COUNT BIGINT(7) NOT NULL,
+ REPEAT_INTERVAL BIGINT(12) NOT NULL,
+ TIMES_TRIGGERED BIGINT(10) NOT NULL,
+ PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
+ FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
+ REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
+);
+
+CREATE TABLE QRTZ_CRON_TRIGGERS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_GROUP VARCHAR(100) NOT NULL,
+ CRON_EXPRESSION VARCHAR(200) NOT NULL,
+ TIME_ZONE_ID VARCHAR(80),
+ PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
+ FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
+ REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
+);
+
+CREATE TABLE QRTZ_SIMPROP_TRIGGERS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_GROUP VARCHAR(100) NOT NULL,
+ STR_PROP_1 VARCHAR(512) NULL,
+ STR_PROP_2 VARCHAR(512) NULL,
+ STR_PROP_3 VARCHAR(512) NULL,
+ INT_PROP_1 INT NULL,
+ INT_PROP_2 INT NULL,
+ LONG_PROP_1 BIGINT NULL,
+ LONG_PROP_2 BIGINT NULL,
+ DEC_PROP_1 NUMERIC(13, 4) NULL,
+ DEC_PROP_2 NUMERIC(13, 4) NULL,
+ BOOL_PROP_1 VARCHAR(1) NULL,
+ BOOL_PROP_2 VARCHAR(1) NULL,
+ PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
+ FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
+ REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
+);
+
+CREATE TABLE QRTZ_BLOB_TRIGGERS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_GROUP VARCHAR(100) NOT NULL,
+ BLOB_DATA BLOB NULL,
+ PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
+ FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
+ REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
+);
+
+CREATE TABLE QRTZ_CALENDARS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ CALENDAR_NAME VARCHAR(100) NOT NULL,
+ CALENDAR BLOB NOT NULL,
+ PRIMARY KEY (SCHED_NAME, CALENDAR_NAME)
+);
+
+CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_GROUP VARCHAR(100) NOT NULL,
+ PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP)
+);
+
+CREATE TABLE QRTZ_FIRED_TRIGGERS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ ENTRY_ID VARCHAR(95) NOT NULL,
+ TRIGGER_NAME VARCHAR(100) NOT NULL,
+ TRIGGER_GROUP VARCHAR(100) NOT NULL,
+ INSTANCE_NAME VARCHAR(100) NOT NULL,
+ FIRED_TIME BIGINT(13) NOT NULL,
+ SCHED_TIME BIGINT(13) NOT NULL,
+ PRIORITY INTEGER NOT NULL,
+ STATE VARCHAR(16) NOT NULL,
+ JOB_NAME VARCHAR(100) NULL,
+ JOB_GROUP VARCHAR(100) NULL,
+ IS_NONCONCURRENT VARCHAR(1) NULL,
+ REQUESTS_RECOVERY VARCHAR(1) NULL,
+ PRIMARY KEY (SCHED_NAME, ENTRY_ID)
+);
+
+CREATE TABLE QRTZ_SCHEDULER_STATE
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ INSTANCE_NAME VARCHAR(100) NOT NULL,
+ LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
+ CHECKIN_INTERVAL BIGINT(13) NOT NULL,
+ PRIMARY KEY (SCHED_NAME, INSTANCE_NAME)
+);
+
+CREATE TABLE QRTZ_LOCKS
+(
+ SCHED_NAME VARCHAR(100) NOT NULL,
+ LOCK_NAME VARCHAR(40) NOT NULL,
+ PRIMARY KEY (SCHED_NAME, LOCK_NAME)
+);
diff --git a/thain-core/src/main/resources/sql/mysql/spring_session.sql b/thain-core/src/main/resources/sql/mysql/spring_session.sql
new file mode 100644
index 00000000..c0af8a8c
--- /dev/null
+++ b/thain-core/src/main/resources/sql/mysql/spring_session.sql
@@ -0,0 +1,32 @@
+
+CREATE TABLE SPRING_SESSION
+(
+ PRIMARY_ID CHAR(36) NOT NULL,
+ SESSION_ID CHAR(36) NOT NULL,
+ CREATION_TIME BIGINT NOT NULL,
+ LAST_ACCESS_TIME BIGINT NOT NULL,
+ MAX_INACTIVE_INTERVAL INT NOT NULL,
+ EXPIRY_TIME BIGINT NOT NULL,
+ PRINCIPAL_NAME VARCHAR(100),
+ CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
+) ENGINE = InnoDB
+ ROW_FORMAT = DYNAMIC;
+
+ALTER TABLE SPRING_SESSION
+ ADD UNIQUE SPRING_SESSION_IX1 (SESSION_ID);
+ALTER TABLE SPRING_SESSION
+ ADD INDEX SPRING_SESSION_IX2 (EXPIRY_TIME);
+ALTER TABLE SPRING_SESSION
+ ADD INDEX SPRING_SESSION_IX3 (PRINCIPAL_NAME);
+
+
+
+CREATE TABLE SPRING_SESSION_ATTRIBUTES
+(
+ SESSION_PRIMARY_ID CHAR(36) NOT NULL,
+ ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
+ ATTRIBUTE_BYTES BLOB NOT NULL,
+ CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
+ CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION (PRIMARY_ID) ON DELETE CASCADE
+) ENGINE = InnoDB
+ ROW_FORMAT = DYNAMIC;
diff --git a/thain-core/src/main/resources/sql/mysql/thain.sql b/thain-core/src/main/resources/sql/mysql/thain.sql
new file mode 100644
index 00000000..b89247c5
--- /dev/null
+++ b/thain-core/src/main/resources/sql/mysql/thain.sql
@@ -0,0 +1,106 @@
+-- Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+-- This source code is licensed under the Apache License Version 2.0, which
+-- can be found in the LICENSE file in the root directory of this source tree.
+
+create table thain_flow
+(
+ id int unsigned auto_increment primary key comment 'flow id,需要唯一,后面对任务的一系列操作都基于它',
+ name varchar(100) default '' not null comment 'Flow名称,显示用',
+ cron varchar(100) default '' not null comment 'cron 表达式',
+ modify_callback_url varchar(512) default '' not null comment '修改回调地址',
+ pause_continuous_failure int unsigned default 0 not null comment '连续失败导致暂停的次数,0表示不暂停',
+ email_continuous_failure varchar(512) default '' not null comment '连续失败pause_continuous_failures次数后,发送邮件的邮箱',
+ create_user varchar(100) default '' not null comment '创建人',
+ callback_url varchar(512) default '' not null comment '状态回调地址',
+ callback_email varchar(512) default '' not null comment '状态回调邮箱',
+ create_app_id varchar(128) default '' not null comment '创建app id, 0为网页上创建',
+ sla_duration int unsigned default 0 not null comment '期待执行的时间(秒)',
+ sla_email varchar(512) default '' not null comment '超过期望时间发通知的收件人,多个用逗号隔开',
+ sla_kill tinyint(1) default 0 not null comment '超过期望时间 是否kill',
+ last_run_status tinyint unsigned default 0 not null comment '最后一次运行状态:
+ 1 未运行、2 运行成功、3 运行异常、4 正在运行、5 手动杀死、6 暂停运行(运行了一半,点了暂停)',
+ scheduling_status tinyint unsigned default 0 not null comment '调度状态:
+ 1 未设置调度,
+ 2 调度中,
+ 3 暂停调度',
+ create_time timestamp default '2019-01-01 00:00:00' not null comment '创建时间',
+ update_time timestamp default '2019-01-01 00:00:00' not null comment '更新时间',
+ status_update_time timestamp default '2019-01-01 00:00:00' not null on update CURRENT_TIMESTAMP comment '状态更新时间',
+ deleted tinyint(1) default 0 not null comment '标记是否删除'
+)
+ ENGINE = InnoDB
+ comment 'flow表';
+
+
+
+create table thain_flow_execution
+(
+ id int unsigned auto_increment primary key comment '自增id',
+ flow_id int unsigned default 0 not null comment '所属flow id',
+ status tinyint unsigned default 1 not null comment '流程执行状态,1 执行中,2 执行结束,3执行异常,4 手动kill',
+ host_info varchar(128) default '' not null comment '机器信息',
+ trigger_type tinyint unsigned default 1 not null comment '触发类型 1手动 2自动',
+ logs mediumtext null comment '日志',
+ create_time timestamp default '2019-01-01 00:00:00' not null comment '创建时间',
+ update_time timestamp default '2019-01-01 00:00:00' not null on update CURRENT_TIMESTAMP comment '更新时间'
+) ENGINE = InnoDB;
+
+create table thain_job
+(
+ id int unsigned auto_increment primary key comment 'id',
+ flow_id int unsigned default 0 not null,
+ name varchar(128) default '0' not null comment 'flow id 对应的name 不能重复',
+ `condition` varchar(256) default '' not null comment '触发条件',
+ component varchar(128) default '' not null comment 'job 所用组件名称',
+ callback_url text null comment '状态回调地址',
+ properties json not null comment '组件属性,json表示',
+ x_axis int default 0 not null comment '横坐标',
+ y_axis int default 0 not null comment '纵坐标',
+ create_time timestamp default CURRENT_TIMESTAMP not null comment 'create time',
+ deleted tinyint(1) default 0 not null comment 'deleted'
+)
+ ENGINE = InnoDB;
+
+create table thain_job_execution
+(
+ id int unsigned auto_increment primary key comment 'id',
+ flow_execution_id int unsigned default 0 not null comment '关联的flow_execution',
+ job_id int unsigned default 0 not null comment 'job id',
+ status tinyint unsigned default 0 not null comment '节点执行状态:1未执行,2执行中,3执行结束,4执行异常',
+ logs mediumtext null comment 'job running logs',
+ create_time timestamp default '2019-01-01 00:00:00' not null comment 'create time',
+ update_time timestamp default '2019-01-01 00:00:00' not null on update CURRENT_TIMESTAMP comment 'update time'
+)
+ ENGINE = InnoDB
+ comment '节点运行表';
+
+
+create table thain_user
+(
+ id int unsigned auto_increment primary key comment '自增id',
+ user_id varchar(100) default '' not null comment '用户id',
+ user_name varchar(100) default '' not null comment '用户名',
+ password_hash varchar(100) default '' not null comment '密码',
+ email varchar(300) default '' not null comment 'email',
+ admin tinyint(1) default 0 not null comment '是否管理员',
+ constraint thain_user_user_id_uindex
+ unique (user_id)
+)
+ ENGINE = InnoDB
+ comment '用户表';
+
+
+create table thain_x5_config
+(
+ id int auto_increment primary key comment 'id',
+ app_id varchar(128) default '' not null comment 'app id',
+ app_key varchar(128) default '' not null comment 'app key',
+ app_name varchar(128) default '' not null comment 'app名称',
+ principal varchar(1024) default '' not null comment '负责人',
+ app_description varchar(512) default '' not null comment '描述',
+ create_time timestamp default CURRENT_TIMESTAMP not null comment 'create time',
+ constraint sch_x5_config_app_id_uindex
+ unique (app_id)
+)
+ ENGINE = InnoDB;
+
diff --git a/thain-fe/.editorconfig b/thain-fe/.editorconfig
new file mode 100644
index 00000000..ea459824
--- /dev/null
+++ b/thain-fe/.editorconfig
@@ -0,0 +1,20 @@
+# Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+# This source code is licensed under the Apache License Version 2.0, which
+# can be found in the LICENSE file in the root directory of this source tree.
+#
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[Makefile]
+indent_style = tab
diff --git a/thain-fe/.eslintignore b/thain-fe/.eslintignore
new file mode 100644
index 00000000..e0f27e45
--- /dev/null
+++ b/thain-fe/.eslintignore
@@ -0,0 +1,4 @@
+/lambda/
+/scripts
+/config
+**/editor.js
diff --git a/thain-fe/.eslintrc.js b/thain-fe/.eslintrc.js
new file mode 100644
index 00000000..b8a0a85f
--- /dev/null
+++ b/thain-fe/.eslintrc.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+module.exports = {
+ parser: 'babel-eslint',
+ extends: ['airbnb', 'prettier', 'plugin:compat/recommended'],
+ env: {
+ browser: true,
+ node: true,
+ es6: true,
+ mocha: true,
+ jest: true,
+ jasmine: true,
+ },
+ globals: {
+ page: true,
+ ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true,
+ },
+ rules: {
+ 'react/jsx-filename-extension': [1, { extensions: ['.js'] }],
+ 'react/jsx-wrap-multilines': 0,
+ 'react/prop-types': 0,
+ 'react/forbid-prop-types': 0,
+ 'react/jsx-one-expression-per-line': 0,
+ 'import/no-unresolved': [2, { ignore: ['^@/', '^umi/'] }],
+ 'import/no-extraneous-dependencies': [
+ 2,
+ {
+ optionalDependencies: true,
+ devDependencies: [],
+ },
+ ],
+ 'import/no-cycle': 0,
+ 'jsx-a11y/no-noninteractive-element-interactions': 0,
+ 'jsx-a11y/click-events-have-key-events': 0,
+ 'jsx-a11y/no-static-element-interactions': 0,
+ 'jsx-a11y/anchor-is-valid': 0,
+ 'linebreak-style': 0,
+ },
+ settings: {
+ // support import modules from TypeScript files in JavaScript files
+ 'import/resolver': { node: { extensions: ['.js', '.ts', '.tsx'] } },
+ polyfills: ['fetch', 'promises', 'url', 'object-assign'],
+ },
+};
diff --git a/thain-fe/.gitignore b/thain-fe/.gitignore
new file mode 100644
index 00000000..f986adbc
--- /dev/null
+++ b/thain-fe/.gitignore
@@ -0,0 +1,38 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+**/node_modules
+# roadhog-api-doc ignore
+/src/utils/request-temp.js
+_roadhog-api-doc
+
+# production
+/dist
+/.vscode
+/build
+
+# misc
+.DS_Store
+npm-debug.log*
+yarn-error.log
+
+/coverage
+.idea
+yarn.lock
+package-lock.json
+*bak
+.vscode
+
+# visual studio code
+.history
+*.log
+functions/*
+.temp/**
+
+# umi
+.umi
+.umi-production
+
+# screenshot
+screenshot
+.firebase
diff --git a/thain-fe/.npmrc b/thain-fe/.npmrc
new file mode 100644
index 00000000..2f5b5dea
--- /dev/null
+++ b/thain-fe/.npmrc
@@ -0,0 +1,11 @@
+# 安装包时锁定版本
+save-prefix="~"
+# 配置 node-sass 源
+sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
+# 配置 taobao 源
+registry=https://registry.npm.taobao.org
+# 配置 @mi: 私有源
+@mi:registry=http://registry.npm.pt.mi.com
+# Electron Mirror of China
+electron_mirror="https://npm.taobao.org/mirrors/electron/"
+
diff --git a/thain-fe/.prettierignore b/thain-fe/.prettierignore
new file mode 100644
index 00000000..d8ad0749
--- /dev/null
+++ b/thain-fe/.prettierignore
@@ -0,0 +1,10 @@
+**/*.svg
+**/*.png
+**/*.eot
+**/*.ttf
+.*
+package.json
+.umi
+.umi-production
+**/editor.js
+/build
diff --git a/thain-fe/.prettierrc b/thain-fe/.prettierrc
new file mode 100644
index 00000000..4d911faa
--- /dev/null
+++ b/thain-fe/.prettierrc
@@ -0,0 +1,20 @@
+{
+ "singleQuote": true,
+ "trailingComma": "all",
+ "printWidth": 100,
+ "proseWrap": "never",
+ "overrides": [
+ {
+ "files": ".prettierrc",
+ "options": {
+ "parser": "json"
+ }
+ },
+ {
+ "files": "document.ejs",
+ "options": {
+ "parser": "html"
+ }
+ }
+ ]
+}
diff --git a/thain-fe/.stylelintrc.json b/thain-fe/.stylelintrc.json
new file mode 100644
index 00000000..215bf081
--- /dev/null
+++ b/thain-fe/.stylelintrc.json
@@ -0,0 +1,13 @@
+{
+ "extends": [
+ "stylelint-config-standard",
+ "stylelint-config-css-modules",
+ "stylelint-config-rational-order",
+ "stylelint-config-prettier"
+ ],
+ "plugins": ["stylelint-order", "stylelint-declaration-block-no-ignored-properties"],
+ "rules": {
+ "no-descending-specificity": null,
+ "plugin/declaration-block-no-ignored-properties": true
+ }
+}
diff --git a/thain-fe/config/config.ts b/thain-fe/config/config.ts
new file mode 100644
index 00000000..bf8f1b93
--- /dev/null
+++ b/thain-fe/config/config.ts
@@ -0,0 +1,165 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+// https://umijs.org/config/
+import os from 'os';
+import slash from 'slash2';
+import { IPlugin, IConfig } from 'umi-types';
+import defaultSettings from './defaultSettings';
+import webpackPlugin from './plugin.config';
+import routerConfig from './router.config';
+
+const { pwa, primaryColor } = defaultSettings;
+// preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
+
+const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION, TEST, NODE_ENV } = process.env;
+const plugins: IPlugin[] = [
+ [
+ 'umi-plugin-react',
+ {
+ antd: true,
+ dva: {
+ hmr: true,
+ },
+ locale: {
+ // default false
+ enable: true,
+ // default zh-CN
+ default: 'zh-CN',
+ // default true, when it is true, will use `navigator.language` overwrite default
+ baseNavigator: true,
+ },
+ dynamicImport: {
+ loadingComponent: './components/PageLoading/index',
+ webpackChunkName: true,
+ level: 3,
+ },
+ pwa: pwa
+ ? {
+ workboxPluginMode: 'InjectManifest',
+ workboxOptions: {
+ importWorkboxFrom: 'local',
+ },
+ }
+ : false,
+ ...(!TEST && os.platform() === 'darwin'
+ ? {
+ dll: {
+ include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch'],
+ exclude: ['@babel/runtime', 'netlify-lambda'],
+ },
+ hardSource: false,
+ }
+ : {}),
+ },
+ ],
+ [
+ 'umi-plugin-pro-block',
+ {
+ moveMock: false,
+ moveService: false,
+ modifyRequest: true,
+ autoAddMenu: true,
+ },
+ ],
+]; // 针对 preview.pro.ant.design 的 GA 统计代码
+// preview.pro.ant.design only do not use in your production ; preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
+
+if (ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
+ plugins.push([
+ 'umi-plugin-ga',
+ {
+ code: 'UA-72788897-6',
+ },
+ ]);
+}
+
+const uglifyJSOptions =
+ NODE_ENV === 'production'
+ ? {
+ uglifyOptions: {
+ // remove console.* except console.error
+ compress: {
+ drop_console: true,
+ pure_funcs: ['console.error'],
+ },
+ },
+ }
+ : {};
+export default {
+ // add for transfer to umi
+ plugins,
+ define: {
+ ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION:
+ ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION || '', // preview.pro.ant.design only do not use in your production ; preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
+ },
+ block: {
+ defaultGitUrl: 'https://github.com/ant-design/pro-blocks',
+ },
+ treeShaking: true,
+ targets: {
+ ie: 11,
+ },
+ devtool: ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION ? 'source-map' : false,
+ // 路由配置
+ routes: routerConfig,
+ // Theme for antd
+ // https://ant.design/docs/react/customize-theme-cn
+ theme: {
+ 'primary-color': primaryColor,
+ },
+ proxy: {
+ '/api/': {
+ target: 'http://localhost:9900/',
+ changeOrigin: true,
+ pathRewrite: { '^/': '' },
+ },
+ },
+ hash: true,
+ outputPath: '../thain-server/src/main/resources/static',
+ publicPath: '/',
+
+ ignoreMomentLocale: true,
+ lessLoaderOptions: {
+ javascriptEnabled: true,
+ },
+ disableRedirectHoist: true,
+ cssLoaderOptions: {
+ modules: true,
+ getLocalIdent: (
+ context: {
+ resourcePath: string;
+ },
+ localIdentName: string,
+ localName: string,
+ ) => {
+ if (
+ context.resourcePath.includes('node_modules') ||
+ context.resourcePath.includes('ant.design.pro.less') ||
+ context.resourcePath.includes('global.less')
+ ) {
+ return localName;
+ }
+
+ const match = context.resourcePath.match(/src(.*)/);
+
+ if (match && match[1]) {
+ const antdProPath = match[1].replace('.less', '');
+ const arr = slash(antdProPath)
+ .split('/')
+ .map((a: string) => a.replace(/([A-Z])/g, '-$1'))
+ .map((a: string) => a.toLowerCase());
+ return `antd-pro${arr.join('-')}-${localName}`.replace(/--/g, '-');
+ }
+
+ return localName;
+ },
+ },
+ manifest: {
+ basePath: '/',
+ },
+ uglifyJSOptions,
+ chainWebpack: webpackPlugin,
+} as IConfig;
diff --git a/thain-fe/config/defaultSettings.ts b/thain-fe/config/defaultSettings.ts
new file mode 100644
index 00000000..2882c8f3
--- /dev/null
+++ b/thain-fe/config/defaultSettings.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { MenuTheme } from 'antd/es/menu';
+
+export type ContentWidth = 'Fluid' | 'Fixed';
+
+export interface DefaultSettings {
+ /**
+ * theme for nav menu
+ */
+ navTheme: MenuTheme;
+ /**
+ * primary color of ant design
+ */
+ primaryColor: string;
+ /**
+ * nav menu position: `sidemenu` or `topmenu`
+ */
+ layout: 'sidemenu' | 'topmenu';
+ /**
+ * layout of content: `Fluid` or `Fixed`, only works when layout is topmenu
+ */
+ contentWidth: ContentWidth;
+ /**
+ * sticky header
+ */
+ fixedHeader: boolean;
+ /**
+ * auto hide header
+ */
+ autoHideHeader: boolean;
+ /**
+ * sticky siderbar
+ */
+ fixSiderbar: boolean;
+ menu: { locale: boolean };
+ title: string;
+ pwa: boolean;
+ // Your custom iconfont Symbol script Url
+ // eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js
+ // 注意:如果需要图标多色,Iconfont 图标项目里要进行批量去色处理
+ // Usage: https://github.com/ant-design/ant-design-pro/pull/3517
+ iconfontUrl: string;
+ colorWeak: boolean;
+}
+
+export default {
+ navTheme: 'dark',
+ primaryColor: '#1890FF',
+ layout: 'topmenu',
+ contentWidth: 'Fluid',
+ fixedHeader: false,
+ autoHideHeader: false,
+ fixSiderbar: false,
+ colorWeak: false,
+ menu: {
+ locale: true,
+ },
+ title: 'Thain',
+ pwa: false,
+ iconfontUrl: '',
+} as DefaultSettings;
diff --git a/thain-fe/config/plugin.config.ts b/thain-fe/config/plugin.config.ts
new file mode 100644
index 00000000..bd4ddfdf
--- /dev/null
+++ b/thain-fe/config/plugin.config.ts
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+// Change theme plugin
+
+import MergeLessPlugin from 'antd-pro-merge-less';
+import AntDesignThemePlugin from 'antd-theme-webpack-plugin';
+import path from 'path';
+
+function getModulePackageName(module: { context: string }) {
+ if (!module.context) return null;
+
+ const nodeModulesPath = path.join(__dirname, '../node_modules/');
+ if (module.context.substring(0, nodeModulesPath.length) !== nodeModulesPath) {
+ return null;
+ }
+
+ const moduleRelativePath = module.context.substring(nodeModulesPath.length);
+ const [moduleDirName] = moduleRelativePath.split(path.sep);
+ let packageName: string | null = moduleDirName;
+ // handle tree shaking
+ if (packageName && packageName.match('^_')) {
+ // eslint-disable-next-line prefer-destructuring
+ packageName = packageName.match(/^_(@?[^@]+)/)![1];
+ }
+ return packageName;
+}
+
+export default (config: any) => {
+ // preview.pro.ant.design only do not use in your production ; preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
+ if (
+ process.env.ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ||
+ process.env.NODE_ENV !== 'production'
+ ) {
+ // 将所有 less 合并为一个供 themePlugin使用
+ const outFile = path.join(__dirname, '../.temp/ant-design-pro.less');
+ const stylesDir = path.join(__dirname, '../src/');
+
+ config.plugin('merge-less').use(MergeLessPlugin, [
+ {
+ stylesDir,
+ outFile,
+ },
+ ]);
+
+ config.plugin('ant-design-theme').use(AntDesignThemePlugin, [
+ {
+ antDir: path.join(__dirname, '../node_modules/antd'),
+ stylesDir,
+ varFile: path.join(__dirname, '../node_modules/antd/lib/style/themes/default.less'),
+ mainLessFile: outFile, // themeVariables: ['@primary-color'],
+ indexFileName: 'index.html',
+ generateOne: true,
+ lessUrl: 'https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js',
+ },
+ ]);
+ }
+ // optimize chunks
+ config.optimization
+ .runtimeChunk(false) // share the same chunks across different modules
+ .splitChunks({
+ chunks: 'async',
+ name: 'vendors',
+ maxInitialRequests: Infinity,
+ minSize: 0,
+ cacheGroups: {
+ vendors: {
+ test: (module: { context: string }) => {
+ const packageName = getModulePackageName(module);
+ if (packageName) {
+ return ['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0;
+ }
+ return false;
+ },
+ name(module: { context: string }) {
+ const packageName = getModulePackageName(module);
+ if (packageName) {
+ if (['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0) {
+ return 'viz'; // visualization package
+ }
+ }
+ return 'misc';
+ },
+ },
+ },
+ });
+};
diff --git a/thain-fe/config/router.config.ts b/thain-fe/config/router.config.ts
new file mode 100644
index 00000000..d1a3fc00
--- /dev/null
+++ b/thain-fe/config/router.config.ts
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default [
+ // user
+ {
+ path: '/user',
+ component: '../layouts/UserLayout',
+ routes: [
+ { path: '/user', redirect: '/user/login' },
+ { path: '/user/login', name: 'login', component: './User/Login' },
+ {
+ component: './Exception/404',
+ },
+ ],
+ },
+ // app
+ {
+ path: '/',
+ component: '../layouts/BasicLayout',
+ Routes: ['src/pages/Authorized'],
+ authority: ['admin', 'user'],
+ routes: [
+ { path: '/', redirect: '/dashboard' },
+ {
+ path: '/dashboard',
+ name: 'dashboard',
+ icon: 'dashboard',
+ component: './Dashboard',
+ },
+ {
+ path: '/flow-editor',
+ name: 'editor',
+ icon: 'edit',
+ component: './FlowEditor',
+ },
+ {
+ path: '/flow-editor/:flowId',
+ component: './FlowEditor',
+ hideInMenu: true,
+ },
+ {
+ path: '/flow/list',
+ icon: 'table',
+ name: 'flows',
+ component: './Flow/List',
+ },
+ {
+ path: '/flow-execution/list',
+ icon: 'profile',
+ name: 'executions',
+ component: './FlowExecution/List',
+ },
+ {
+ path: '/flow-execution/list/:flowId',
+ component: './FlowExecution/List',
+ hideInMenu: true,
+ },
+ {
+ path: '/admin',
+ icon: 'table',
+ name: 'admin',
+ component: './admin',
+ authority: ['admin'],
+ },
+ {
+ hideInMenu: true,
+ name: 'exception',
+ icon: 'warning',
+ path: '/exception',
+ routes: [
+ // exception
+ {
+ path: '/exception/403',
+ name: 'not-permission',
+ component: './Exception/403',
+ },
+ {
+ path: '/exception/404',
+ name: 'not-find',
+ component: './Exception/404',
+ },
+ {
+ path: '/exception/500',
+ name: 'server-error',
+ component: './Exception/500',
+ },
+ ],
+ },
+ {
+ component: './Exception/404',
+ },
+ ],
+ },
+];
diff --git a/thain-fe/jsconfig.json b/thain-fe/jsconfig.json
new file mode 100644
index 00000000..f87334d4
--- /dev/null
+++ b/thain-fe/jsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
diff --git a/thain-fe/package.json b/thain-fe/package.json
new file mode 100644
index 00000000..f14ff5d0
--- /dev/null
+++ b/thain-fe/package.json
@@ -0,0 +1,139 @@
+{
+ "name": "thain",
+ "version": "1.0.0",
+ "private": true,
+ "description": "An out-of-box UI solution for enterprise applications",
+ "scripts": {
+ "analyze": "cross-env ANALYZE=1 umi build",
+ "build": "umi build",
+ "fetch:blocks": "node ./scripts/fetch-blocks.js",
+ "lint": "npm run lint:js && npm run lint:ts && npm run lint:style && npm run lint:prettier",
+ "lint-staged": "lint-staged",
+ "lint-staged:js": "eslint --ext .js",
+ "lint-staged:ts": "tslint",
+ "lint:fix": "eslint --fix --ext .js src tests && npm run lint:style && npm run tslint:fix",
+ "lint:js": "eslint --ext .js src tests",
+ "lint:prettier": "check-prettier lint",
+ "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
+ "lint:ts": "tslint -p . -c ./tslint.yml",
+ "prettier": "prettier -c --write '**/*'",
+ "start": "umi dev",
+ "test": "umi test",
+ "test:all": "node ./tests/run-tests.js",
+ "test:component": "umi test ./src/components",
+ "tslint:fix": "tslint --fix \"src/**/*.ts*\""
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "npm run lint-staged"
+ }
+ },
+ "lint-staged": {
+ "**/*.less": "stylelint --syntax less",
+ "**/*.{js,jsx}": "npm run lint-staged:js",
+ "**/*.{ts,tsx}": "npm run lint-staged:ts",
+ "**/*.{js,jsx,tsx,ts,less,md,json}": [
+ "prettier --write",
+ "git add"
+ ]
+ },
+ "browserslist": [
+ "> 1%",
+ "last 2 versions",
+ "not ie <= 10"
+ ],
+ "dependencies": {
+ "@ant-design/pro-layout": "~4.5.16",
+ "@antv/data-set": "^0.10.2",
+ "antd": "~3.24.2",
+ "bizcharts": "~3.5.5",
+ "braft-editor": "~2.3.8",
+ "braft-extensions": "0.0.20",
+ "classnames": "^2.2.6",
+ "codemirror": "~5.49.2",
+ "core-js": "~3.3.3",
+ "dva": "^2.4.1",
+ "lodash": "~4.17.15",
+ "lodash-decorators": "^6.0.1",
+ "memoize-one": "~5.1.1",
+ "moment": "^2.24.0",
+ "omit.js": "^1.0.2",
+ "path-to-regexp": "~3.1.0",
+ "prop-types": "^15.7.2",
+ "qs": "~6.9.0",
+ "rc-animate": "~2.10.0",
+ "react": "~16.11.0",
+ "react-codemirror2": "^6.0.0",
+ "react-container-query": "^0.11.0",
+ "react-copy-to-clipboard": "^5.0.1",
+ "react-document-title": "^2.0.3",
+ "react-dom": "~16.11.0",
+ "react-file-base64": "~1.0.3",
+ "react-media": "~1.10.0",
+ "react-media-hook2": "^1.1.2",
+ "umi": "~2.10.7",
+ "umi-plugin-ga": "~1.1.5",
+ "umi-plugin-locale": "~2.11.2",
+ "umi-plugin-pro-block": "~1.3.4",
+ "umi-plugin-react": "~1.12.8",
+ "umi-request": "~1.2.8"
+ },
+ "devDependencies": {
+ "@types/classnames": "~2.2.9",
+ "@types/history": "^4.7.2",
+ "@types/lodash": "~4.14.137",
+ "@types/qs": "^6.5.3",
+ "@types/react": "~16.9.9",
+ "@types/react-document-title": "^2.0.3",
+ "@types/react-dom": "~16.9.2",
+ "antd-pro-merge-less": "^1.0.0",
+ "antd-theme-webpack-plugin": "^1.3.0",
+ "babel-eslint": "~10.0.3",
+ "chalk": "^2.4.2",
+ "check-prettier": "^1.0.3",
+ "cross-env": "~6.0.0",
+ "cross-port-killer": "^1.1.1",
+ "enzyme": "^3.10.0",
+ "eslint": "~6.5.1",
+ "eslint-config-airbnb": "~18.0.0",
+ "eslint-config-prettier": "~6.4.0",
+ "eslint-plugin-babel": "^5.3.0",
+ "eslint-plugin-compat": "~3.3.0",
+ "eslint-plugin-import": "~2.18.2",
+ "eslint-plugin-jsx-a11y": "~6.2.3",
+ "eslint-plugin-markdown": "~1.0.1",
+ "eslint-plugin-react": "~7.16.0",
+ "gh-pages": "~2.1.1",
+ "husky": "~3.0.9",
+ "jsdom-global": "^3.0.2",
+ "less": "~3.10.3",
+ "lint-staged": "~9.4.2",
+ "node-fetch": "^2.6.0",
+ "prettier": "^1.18.2",
+ "slash2": "^2.0.0",
+ "stylelint": "~11.1.1",
+ "stylelint-config-css-modules": "~1.5.0",
+ "stylelint-config-prettier": "~6.0.0",
+ "stylelint-config-rational-order": "^0.1.2",
+ "stylelint-config-standard": "~19.0.0",
+ "stylelint-declaration-block-no-ignored-properties": "^2.1.0",
+ "stylelint-order": "~3.1.1",
+ "tslint": "~5.20.0",
+ "tslint-config-prettier": "^1.18.0",
+ "tslint-eslint-rules": "^5.4.0",
+ "tslint-react": "~4.1.0"
+ },
+ "optionalDependencies": {
+ "puppeteer": "~1.18.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "checkFiles": [
+ "src/**/*.js*",
+ "src/**/*.ts*",
+ "src/**/*.less",
+ "config/**/*.js*",
+ "scripts/**/*.js"
+ ]
+}
diff --git a/thain-fe/public/favicon.ico b/thain-fe/public/favicon.ico
new file mode 100644
index 00000000..e2492ff5
Binary files /dev/null and b/thain-fe/public/favicon.ico differ
diff --git a/thain-fe/src/assets/logo.svg b/thain-fe/src/assets/logo.svg
new file mode 100644
index 00000000..e9f8c2a9
--- /dev/null
+++ b/thain-fe/src/assets/logo.svg
@@ -0,0 +1,43 @@
+
+
\ No newline at end of file
diff --git a/thain-fe/src/assets/xdata_logo.png b/thain-fe/src/assets/xdata_logo.png
new file mode 100644
index 00000000..41deee94
Binary files /dev/null and b/thain-fe/src/assets/xdata_logo.png differ
diff --git a/thain-fe/src/commonModels/FlowAllInfo.ts b/thain-fe/src/commonModels/FlowAllInfo.ts
new file mode 100644
index 00000000..927411e5
--- /dev/null
+++ b/thain-fe/src/commonModels/FlowAllInfo.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { FlowModel } from './FlowModel';
+import { JobModel } from './JobModel';
+
+export interface FlowAllInfo {
+ flowModel: FlowModel;
+ jobModelList: JobModel[];
+}
diff --git a/thain-fe/src/commonModels/FlowExecutionAllInfo.ts b/thain-fe/src/commonModels/FlowExecutionAllInfo.ts
new file mode 100644
index 00000000..55113b31
--- /dev/null
+++ b/thain-fe/src/commonModels/FlowExecutionAllInfo.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { JobModel } from './JobModel';
+import { FlowExecutionModel } from './FlowExecutionModel';
+import { JobExecutionModel } from './JobExecutionModel';
+
+export class FlowExecutionAllInfo {
+ flowExecutionModel: FlowExecutionModel = new FlowExecutionModel();
+ jobModelList: JobModel[] = [];
+ jobExecutionModelList: JobExecutionModel[] = [];
+}
diff --git a/thain-fe/src/commonModels/FlowExecutionModel.ts b/thain-fe/src/commonModels/FlowExecutionModel.ts
new file mode 100644
index 00000000..5fd353ad
--- /dev/null
+++ b/thain-fe/src/commonModels/FlowExecutionModel.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+/**
+ * 数据库中的 thain_flow_execution
+ */
+export class FlowExecutionModel {
+ id = 0;
+ flowId = 0;
+ status = 0;
+ hostInfo = '';
+ triggerType = 0;
+ logs = '';
+ createTime = 0;
+ updateTime = 0;
+}
diff --git a/thain-fe/src/commonModels/FlowModel.ts b/thain-fe/src/commonModels/FlowModel.ts
new file mode 100644
index 00000000..e96248e6
--- /dev/null
+++ b/thain-fe/src/commonModels/FlowModel.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+/**
+ * 数据库中的thain_flow
+ */
+export class FlowModel {
+ id?: number;
+ name = '';
+ cron?: string;
+ createUser?: string;
+ callbackEmail?: string;
+ callbackUrl?: string;
+ createAppId?: string;
+ lastRunStatus?: number;
+ schedulingStatus?: number;
+ public slaDuration?: number;
+ public slaEmail?: string;
+ public slaKill?: boolean;
+ createTime?: number;
+ updateTime?: number;
+ statusUpdateTime?: number;
+}
diff --git a/thain-fe/src/commonModels/JobExecutionModel.ts b/thain-fe/src/commonModels/JobExecutionModel.ts
new file mode 100644
index 00000000..57813568
--- /dev/null
+++ b/thain-fe/src/commonModels/JobExecutionModel.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+/**
+ * 数据库中的thain_job_execution
+ */
+export interface JobExecutionModel {
+ id: number;
+ /**
+ * 关联job的名称
+ */
+ name?: string;
+ flowExecutionId: number;
+ jobId: number;
+ status: number;
+ logs: string;
+ createTime: number;
+ updateTime: number;
+}
diff --git a/thain-fe/src/commonModels/JobModel.ts b/thain-fe/src/commonModels/JobModel.ts
new file mode 100644
index 00000000..7da0a6c0
--- /dev/null
+++ b/thain-fe/src/commonModels/JobModel.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { EditorNode } from '@/pages/FlowEditor/EditorNode';
+
+/**
+ * 数据库中的thain_job
+ */
+export class JobModel {
+ static getInstance(node: EditorNode): any {
+ const instance = new JobModel();
+ instance.name = node.label;
+ instance.condition = node.condition;
+ instance.component = node.category;
+ instance.callbackUrl = node.callbackUrl;
+ if (node.attributes) {
+ instance.properties = node.attributes;
+ }
+ instance.xAxis = Math.floor(node.x);
+ instance.yAxis = Math.floor(node.y);
+ return instance;
+ }
+ id?: number;
+ flowId?: number;
+ name = '';
+ condition = '';
+ component = '';
+ callbackUrl = '';
+ properties: { [props: string]: string } = {};
+ xAxis = 0;
+ yAxis = 0;
+ createTime?: number;
+}
diff --git a/thain-fe/src/components/Authorized/Authorized.tsx b/thain-fe/src/components/Authorized/Authorized.tsx
new file mode 100644
index 00000000..1c5e35d3
--- /dev/null
+++ b/thain-fe/src/components/Authorized/Authorized.tsx
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import CheckPermissions from './CheckPermissions';
+import { IAuthorityType } from './CheckPermissions';
+import Secured from './Secured';
+import check from './CheckPermissions';
+import AuthorizedRoute from './AuthorizedRoute';
+import React from 'react';
+
+interface IAuthorizedProps {
+ authority: IAuthorityType;
+ noMatch?: React.ReactNode;
+}
+
+type IAuthorizedType = React.FunctionComponent & {
+ Secured: typeof Secured;
+ check: typeof check;
+ AuthorizedRoute: typeof AuthorizedRoute;
+};
+
+const Authorized: React.FunctionComponent = ({
+ children,
+ authority,
+ noMatch = null,
+}) => {
+ const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children;
+ const dom = CheckPermissions(authority, childrenRender, noMatch);
+ return <>{dom}>;
+};
+
+export default Authorized as IAuthorizedType;
diff --git a/thain-fe/src/components/Authorized/AuthorizedRoute.tsx b/thain-fe/src/components/Authorized/AuthorizedRoute.tsx
new file mode 100644
index 00000000..b7141c59
--- /dev/null
+++ b/thain-fe/src/components/Authorized/AuthorizedRoute.tsx
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+import { Redirect } from 'umi';
+import { Route } from 'react-router-dom';
+import Authorized from './Authorized';
+import { IAuthorityType } from './CheckPermissions';
+
+interface IAuthorizedRoutePops {
+ currentAuthority: string;
+ component: React.ComponentClass;
+ render: (props: any) => React.ReactNode;
+ redirectPath: string;
+ authority: IAuthorityType;
+}
+
+const AuthorizedRoute: React.FunctionComponent = ({
+ component: Component,
+ render,
+ authority,
+ redirectPath,
+ ...rest
+}) => (
+ } />}
+ >
+ (Component ? : render(props))}
+ />
+
+);
+
+export default AuthorizedRoute;
diff --git a/thain-fe/src/components/Authorized/CheckPermissions.tsx b/thain-fe/src/components/Authorized/CheckPermissions.tsx
new file mode 100644
index 00000000..3d7120b6
--- /dev/null
+++ b/thain-fe/src/components/Authorized/CheckPermissions.tsx
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+// eslint-disable-next-line import/no-cycle
+import PromiseRender from './PromiseRender';
+import { CURRENT } from './renderAuthorize';
+
+export type IAuthorityType =
+ | undefined
+ | string
+ | string[]
+ | Promise
+ | ((currentAuthority: string | string[]) => IAuthorityType);
+
+/**
+ * 通用权限检查方法
+ * Common check permissions method
+ * @param { 权限判定 | Permission judgment } authority
+ * @param { 你的权限 | Your permission description } currentAuthority
+ * @param { 通过的组件 | Passing components } target
+ * @param { 未通过的组件 | no pass components } Exception
+ */
+const checkPermissions = (
+ authority: IAuthorityType,
+ currentAuthority: string | string[],
+ target: T,
+ Exception: K,
+): T | K | React.ReactNode => {
+ // 没有判定权限.默认查看所有
+ // Retirement authority, return target;
+ if (!authority) {
+ return target;
+ }
+ // 数组处理
+ if (Array.isArray(authority)) {
+ if (Array.isArray(currentAuthority)) {
+ if (currentAuthority.some(item => authority.includes(item))) {
+ return target;
+ }
+ } else if (authority.includes(currentAuthority)) {
+ return target;
+ }
+ return Exception;
+ }
+ // string 处理
+ if (typeof authority === 'string') {
+ if (Array.isArray(currentAuthority)) {
+ if (currentAuthority.some(item => authority === item)) {
+ return target;
+ }
+ } else if (authority === currentAuthority) {
+ return target;
+ }
+ return Exception;
+ }
+ // Promise 处理
+ if (authority instanceof Promise) {
+ return ok={target} error={Exception} promise={authority} />;
+ }
+ // Function 处理
+ if (typeof authority === 'function') {
+ try {
+ const bool = authority(currentAuthority);
+ // 函数执行后返回值是 Promise
+ if (bool instanceof Promise) {
+ return ok={target} error={Exception} promise={bool} />;
+ }
+ if (bool) {
+ return target;
+ }
+ return Exception;
+ } catch (error) {
+ throw error;
+ }
+ }
+ throw new Error('unsupported parameters');
+};
+
+export { checkPermissions };
+
+function check(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode {
+ return checkPermissions(authority, CURRENT, target, Exception);
+}
+
+export default check;
diff --git a/thain-fe/src/components/Authorized/PromiseRender.tsx b/thain-fe/src/components/Authorized/PromiseRender.tsx
new file mode 100644
index 00000000..ca451363
--- /dev/null
+++ b/thain-fe/src/components/Authorized/PromiseRender.tsx
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { Spin } from 'antd';
+import isEqual from 'lodash/isEqual';
+import React from 'react';
+// eslint-disable-next-line import/no-cycle
+import { isComponentClass } from './Secured';
+
+interface IPromiseRenderProps {
+ ok: T;
+ error: K;
+ promise: Promise;
+}
+
+interface IPromiseRenderState {
+ component: React.ComponentClass | React.FunctionComponent;
+}
+
+export default class PromiseRender extends React.Component<
+ IPromiseRenderProps,
+ IPromiseRenderState
+> {
+ state: IPromiseRenderState = {
+ component: () => null,
+ };
+
+ componentDidMount() {
+ this.setRenderComponent(this.props);
+ }
+
+ shouldComponentUpdate = (
+ nextProps: IPromiseRenderProps,
+ nextState: IPromiseRenderState,
+ ) => {
+ const { component } = this.state;
+ if (!isEqual(nextProps, this.props)) {
+ this.setRenderComponent(nextProps);
+ }
+ if (nextState.component !== component) return true;
+ return false;
+ };
+
+ // set render Component : ok or error
+ setRenderComponent(props: IPromiseRenderProps) {
+ const ok = this.checkIsInstantiation(props.ok);
+ const error = this.checkIsInstantiation(props.error);
+ props.promise
+ .then(() => {
+ this.setState({
+ component: ok,
+ });
+ })
+ .catch(() => {
+ this.setState({
+ component: error,
+ });
+ });
+ }
+
+ // Determine whether the incoming component has been instantiated
+ // AuthorizedRoute is already instantiated
+ // Authorized render is already instantiated, children is no instantiated
+ // Secured is not instantiated
+ checkIsInstantiation = (
+ target: React.ReactNode | React.ComponentClass,
+ ): React.FunctionComponent => {
+ if (isComponentClass(target)) {
+ const Target = target as React.ComponentClass;
+ return (props: any) => ;
+ }
+ if (React.isValidElement(target)) {
+ return (props: any) => React.cloneElement(target, props);
+ }
+ return () => target as (React.ReactNode & null);
+ };
+
+ render() {
+ const { component: Component } = this.state;
+ const { ok, error, promise, ...rest } = this.props;
+ return Component ? (
+
+ ) : (
+
+
+
+ );
+ }
+}
diff --git a/thain-fe/src/components/Authorized/Secured.tsx b/thain-fe/src/components/Authorized/Secured.tsx
new file mode 100644
index 00000000..3963a33f
--- /dev/null
+++ b/thain-fe/src/components/Authorized/Secured.tsx
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+import CheckPermissions from './CheckPermissions';
+
+/**
+ * 默认不能访问任何页面
+ * default is "NULL"
+ */
+const Exception403 = () => 403;
+
+export const isComponentClass = (
+ component: React.ComponentClass | React.ReactNode,
+): boolean => {
+ if (!component) return false;
+ const proto = Object.getPrototypeOf(component);
+ if (proto === React.Component || proto === Function.prototype) return true;
+ return isComponentClass(proto);
+};
+
+// Determine whether the incoming component has been instantiated
+// AuthorizedRoute is already instantiated
+// Authorized render is already instantiated, children is no instantiated
+// Secured is not instantiated
+const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => {
+ if (isComponentClass(target)) {
+ const Target = target as React.ComponentClass;
+ return (props: any) => ;
+ }
+ if (React.isValidElement(target)) {
+ return (props: any) => React.cloneElement(target, props);
+ }
+ return () => target;
+};
+
+/**
+ * 用于判断是否拥有权限访问此 view 权限
+ * authority 支持传入 string, () => boolean | Promise
+ * e.g. 'user' 只有 user 用户能访问
+ * e.g. 'user,admin' user 和 admin 都能访问
+ * e.g. ()=>boolean 返回true能访问,返回false不能访问
+ * e.g. Promise then 能访问 catch不能访问
+ * e.g. authority support incoming string, () => boolean | Promise
+ * e.g. 'user' only user user can access
+ * e.g. 'user, admin' user and admin can access
+ * e.g. () => boolean true to be able to visit, return false can not be accessed
+ * e.g. Promise then can not access the visit to catch
+ * @param {string | function | Promise} authority
+ * @param {ReactNode} error 非必需参数
+ */
+const authorize = (authority: string, error?: React.ReactNode) => {
+ /**
+ * conversion into a class
+ * 防止传入字符串时找不到staticContext造成报错
+ * String parameters can cause staticContext not found error
+ */
+ let classError: boolean | React.FunctionComponent = false;
+ if (error) {
+ classError = (() => error) as React.FunctionComponent;
+ }
+ if (!authority) {
+ throw new Error('authority is required');
+ }
+ return function decideAuthority(target: React.ComponentClass | React.ReactNode) {
+ const component = CheckPermissions(authority, target, classError || Exception403);
+ return checkIsInstantiation(component);
+ };
+};
+
+export default authorize;
diff --git a/thain-fe/src/components/Authorized/index.tsx b/thain-fe/src/components/Authorized/index.tsx
new file mode 100644
index 00000000..1c61e99a
--- /dev/null
+++ b/thain-fe/src/components/Authorized/index.tsx
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import Authorized from './Authorized';
+import AuthorizedRoute from './AuthorizedRoute';
+import Secured from './Secured';
+import check from './CheckPermissions';
+import renderAuthorize from './renderAuthorize';
+
+Authorized.Secured = Secured;
+Authorized.AuthorizedRoute = AuthorizedRoute;
+Authorized.check = check;
+
+const RenderAuthorize = renderAuthorize(Authorized);
+
+export default RenderAuthorize;
diff --git a/thain-fe/src/components/Authorized/renderAuthorize.ts b/thain-fe/src/components/Authorized/renderAuthorize.ts
new file mode 100644
index 00000000..4c7f2ad4
--- /dev/null
+++ b/thain-fe/src/components/Authorized/renderAuthorize.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+let CURRENT: string | string[] = 'NULL';
+type CurrentAuthorityType = string | string[] | (() => typeof CURRENT);
+/**
+ * use authority or getAuthority
+ * @param {string|()=>String} currentAuthority
+ */
+const renderAuthorize = (Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => (
+ currentAuthority: CurrentAuthorityType,
+) => {
+ if (currentAuthority) {
+ if (typeof currentAuthority === 'function') {
+ CURRENT = currentAuthority();
+ }
+ if (
+ Object.prototype.toString.call(currentAuthority) === '[object String]' ||
+ Array.isArray(currentAuthority)
+ ) {
+ CURRENT = currentAuthority as string[];
+ }
+ } else {
+ CURRENT = 'NULL';
+ }
+ return Authorized;
+};
+
+export { CURRENT };
+export default (Authorized: T) => renderAuthorize(Authorized);
diff --git a/thain-fe/src/components/Exception/index.less b/thain-fe/src/components/Exception/index.less
new file mode 100644
index 00000000..8eb5c58f
--- /dev/null
+++ b/thain-fe/src/components/Exception/index.less
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+@import '~antd/lib/style/themes/default.less';
+
+.exception {
+ display: flex;
+ align-items: center;
+ height: 80%;
+ min-height: 500px;
+
+ .imgBlock {
+ flex: 0 0 62.5%;
+ width: 62.5%;
+ padding-right: 152px;
+ zoom: 1;
+
+ &::before,
+ &::after {
+ display: table;
+ content: ' ';
+ }
+
+ &::after {
+ clear: both;
+ height: 0;
+ font-size: 0;
+ visibility: hidden;
+ }
+ }
+
+ .imgEle {
+ float: right;
+ width: 100%;
+ max-width: 430px;
+ height: 360px;
+ background-repeat: no-repeat;
+ background-position: 50% 50%;
+ background-size: contain;
+ }
+
+ .content {
+ flex: auto;
+
+ h1 {
+ margin-bottom: 24px;
+ color: #434e59;
+ font-weight: 600;
+ font-size: 72px;
+ line-height: 72px;
+ }
+
+ .desc {
+ margin-bottom: 16px;
+ color: @text-color-secondary;
+ font-size: 20px;
+ line-height: 28px;
+ }
+
+ .actions {
+ button:not(:last-child) {
+ margin-right: 8px;
+ }
+ }
+ }
+}
+
+@media screen and (max-width: @screen-xl) {
+ .exception {
+ .imgBlock {
+ padding-right: 88px;
+ }
+ }
+}
+
+@media screen and (max-width: @screen-sm) {
+ .exception {
+ display: block;
+ text-align: center;
+
+ .imgBlock {
+ margin: 0 auto 24px;
+ padding-right: 0;
+ }
+ }
+}
+
+@media screen and (max-width: @screen-xs) {
+ .exception {
+ .imgBlock {
+ margin-bottom: -24px;
+ overflow: hidden;
+ }
+ }
+}
diff --git a/thain-fe/src/components/Exception/index.tsx b/thain-fe/src/components/Exception/index.tsx
new file mode 100644
index 00000000..aa4260d0
--- /dev/null
+++ b/thain-fe/src/components/Exception/index.tsx
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { Button } from 'antd';
+import classNames from 'classnames';
+import * as H from 'history';
+import React, { createElement } from 'react';
+import styles from './index.less';
+import config from './typeConfig';
+import Link from 'umi/link';
+
+export interface ExceptionProps<
+ L = {
+ to: H.LocationDescriptor;
+ href?: H.LocationDescriptor;
+ replace?: boolean;
+ innerRef?: (node: HTMLAnchorElement | null) => void;
+ }
+> {
+ type?: '403' | '404' | '500';
+ title?: React.ReactNode;
+ desc?: React.ReactNode;
+ img?: string;
+ actions?: React.ReactNode;
+ linkElement?: string | React.ComponentType | typeof Link;
+ style?: React.CSSProperties;
+ className?: string;
+ backText?: React.ReactNode;
+ redirect?: string;
+}
+
+class Exception extends React.Component {
+ static defaultProps = {
+ backText: 'back to home',
+ redirect: '/',
+ };
+
+ constructor(props: ExceptionProps) {
+ super(props);
+ this.state = {};
+ }
+
+ render() {
+ const {
+ className,
+ backText,
+ linkElement = 'a',
+ type = '404',
+ title,
+ desc,
+ img,
+ actions,
+ redirect,
+ ...rest
+ } = this.props;
+ const pageType = type in config ? type : '404';
+ const clsString = classNames(styles.exception, className);
+ return (
+
+
+
+
{title || config[pageType].title}
+
{desc || config[pageType].desc}
+
+ {actions ||
+ createElement(
+ linkElement as any,
+ {
+ to: redirect,
+ href: redirect,
+ },
+ ,
+ )}
+
+
+
+ );
+ }
+}
+
+export default Exception;
diff --git a/thain-fe/src/components/Exception/typeConfig.ts b/thain-fe/src/components/Exception/typeConfig.ts
new file mode 100644
index 00000000..d4c2775a
--- /dev/null
+++ b/thain-fe/src/components/Exception/typeConfig.ts
@@ -0,0 +1,49 @@
+import { formatMessage } from 'umi-plugin-react/locale';
+
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+interface Config {
+ 403: {
+ img: string;
+ title: string;
+ desc: string;
+ };
+ 404: {
+ img: string;
+ title: string;
+ desc: string;
+ };
+ 500: {
+ img: string;
+ title: string;
+ desc: string;
+ };
+}
+const config: Config = {
+ 403: {
+ img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',
+ title: '403',
+ desc: formatMessage({
+ id: 'global.deny.messgae',
+ }),
+ },
+ 404: {
+ img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
+ title: '404',
+ desc: formatMessage({
+ id: 'global.absent.message',
+ }),
+ },
+ 500: {
+ img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',
+ title: '500',
+ desc: formatMessage({
+ id: 'global.server.error.message',
+ }),
+ },
+};
+
+export default config;
diff --git a/thain-fe/src/components/GlobalFooter/index.less b/thain-fe/src/components/GlobalFooter/index.less
new file mode 100644
index 00000000..455519ad
--- /dev/null
+++ b/thain-fe/src/components/GlobalFooter/index.less
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+@import '~antd/lib/style/themes/default.less';
+
+.globalFooter {
+ margin: 48px 0 24px 0;
+ padding: 0 16px;
+ text-align: center;
+
+ .links {
+ margin-bottom: 8px;
+
+ a {
+ color: @text-color-secondary;
+ transition: all 0.3s;
+
+ &:not(:last-child) {
+ margin-right: 40px;
+ }
+
+ &:hover {
+ color: @text-color;
+ }
+ }
+ }
+
+ .copyright {
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+ }
+}
diff --git a/thain-fe/src/components/GlobalFooter/index.tsx b/thain-fe/src/components/GlobalFooter/index.tsx
new file mode 100644
index 00000000..de566496
--- /dev/null
+++ b/thain-fe/src/components/GlobalFooter/index.tsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+import classNames from 'classnames';
+import styles from './index.less';
+
+export interface GlobalFooterProps {
+ links?: Array<{
+ key?: string;
+ title: React.ReactNode;
+ href: string;
+ blankTarget?: boolean;
+ }>;
+ copyright?: React.ReactNode;
+ style?: React.CSSProperties;
+ className?: string;
+}
+
+const GlobalFooter: React.FC = ({ className, links, copyright }) => {
+ const clsString = classNames(styles.globalFooter, className);
+ return (
+
+ );
+};
+
+export default GlobalFooter;
diff --git a/thain-fe/src/components/GlobalHeader/AvatarDropdown.tsx b/thain-fe/src/components/GlobalHeader/AvatarDropdown.tsx
new file mode 100644
index 00000000..bc5f2fcf
--- /dev/null
+++ b/thain-fe/src/components/GlobalHeader/AvatarDropdown.tsx
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+import { Avatar, Menu, Spin, Icon } from 'antd';
+import { FormattedMessage } from 'umi-plugin-react/locale';
+import { ClickParam } from 'antd/lib/menu';
+import { ConnectProps, ConnectState } from '@/models/connect';
+import { CurrentUser } from '@/models/user';
+import { connect } from 'dva';
+import router from 'umi/router';
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+export interface GlobalHeaderRightProps extends ConnectProps {
+ currentUser?: CurrentUser;
+ menu?: boolean;
+}
+
+class AvatarDropdown extends React.Component {
+ onMenuClick = (event: ClickParam) => {
+ const { key } = event;
+
+ if (key === 'logout') {
+ const { dispatch } = this.props;
+ if (dispatch) {
+ dispatch({
+ type: 'login/logout',
+ });
+ }
+
+ return;
+ }
+ router.push(`/account/${key}`);
+ };
+ render() {
+ const { currentUser = {} } = this.props;
+ const menuHeaderDropdown = (
+
+ );
+
+ return currentUser && currentUser.name ? (
+
+
+
+ {currentUser.name}
+
+
+ ) : (
+
+ );
+ }
+}
+export default connect(({ user }: ConnectState) => ({
+ currentUser: user.currentUser,
+}))(AvatarDropdown);
diff --git a/thain-fe/src/components/GlobalHeader/RightContent.tsx b/thain-fe/src/components/GlobalHeader/RightContent.tsx
new file mode 100644
index 00000000..ca7f3d48
--- /dev/null
+++ b/thain-fe/src/components/GlobalHeader/RightContent.tsx
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { ConnectProps, ConnectState } from '@/models/connect';
+import React, { Component } from 'react';
+import { Icon, Tooltip } from 'antd';
+import { formatMessage } from 'umi-plugin-react/locale';
+import SelectLang from '../SelectLang';
+import styles from './index.less';
+import Avatar from './AvatarDropdown';
+import { connect } from 'dva';
+
+export type SiderTheme = 'light' | 'dark';
+export interface GlobalHeaderRightProps extends ConnectProps {
+ theme?: SiderTheme;
+ layout: 'sidemenu' | 'topmenu';
+}
+
+class GlobalHeaderRight extends Component {
+ render() {
+ const { theme, layout } = this.props;
+ let className = styles.right;
+
+ if (theme === 'dark' && layout === 'topmenu') {
+ className = `${styles.right} ${styles.dark}`;
+ }
+
+ return (
+
+ );
+ }
+}
+
+export default connect(({ settings }: ConnectState) => ({
+ theme: settings.navTheme,
+ layout: settings.layout,
+}))(GlobalHeaderRight);
diff --git a/thain-fe/src/components/GlobalHeader/index.less b/thain-fe/src/components/GlobalHeader/index.less
new file mode 100644
index 00000000..5ef7c0db
--- /dev/null
+++ b/thain-fe/src/components/GlobalHeader/index.less
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+@import '~antd/lib/style/themes/default.less';
+
+@pro-header-hover-bg: rgba(0, 0, 0, 0.025);
+
+.logo {
+ display: inline-block;
+ height: @layout-header-height;
+ padding: 0 0 0 24px;
+ font-size: 20px;
+ line-height: @layout-header-height;
+ vertical-align: top;
+ cursor: pointer;
+ img {
+ display: inline-block;
+ vertical-align: middle;
+ }
+}
+
+.menu {
+ :global(.anticon) {
+ margin-right: 8px;
+ }
+ :global(.ant-dropdown-menu-item) {
+ min-width: 160px;
+ }
+}
+
+.trigger {
+ height: @layout-header-height;
+ padding: ~'calc((@{layout-header-height} - 20px) / 2)' 24px;
+ font-size: 20px;
+ cursor: pointer;
+ transition: all 0.3s, padding 0s;
+ &:hover {
+ background: @pro-header-hover-bg;
+ }
+}
+
+.right {
+ float: right;
+ height: 100%;
+ overflow: hidden;
+ .action {
+ display: inline-block;
+ height: 100%;
+ padding: 0 12px;
+ cursor: pointer;
+ transition: all 0.3s;
+ > i {
+ color: @text-color;
+ vertical-align: middle;
+ }
+ &:hover {
+ background: @pro-header-hover-bg;
+ }
+ &:global(.opened) {
+ background: @pro-header-hover-bg;
+ }
+ }
+ .search {
+ padding: 0 12px;
+ &:hover {
+ background: transparent;
+ }
+ }
+ .account {
+ .avatar {
+ margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0;
+ margin-right: 8px;
+ color: @primary-color;
+ vertical-align: top;
+ background: rgba(255, 255, 255, 0.85);
+ }
+ }
+}
+
+.dark {
+ height: @layout-header-height;
+ .action {
+ color: rgba(255, 255, 255, 0.85);
+ > i {
+ color: rgba(255, 255, 255, 0.85);
+ }
+ &:hover,
+ &:global(.opened) {
+ background: @primary-color;
+ }
+ :global(.ant-badge) {
+ color: rgba(255, 255, 255, 0.85);
+ }
+ }
+}
+
+@media only screen and (max-width: @screen-md) {
+ :global(.ant-divider-vertical) {
+ vertical-align: unset;
+ }
+ .name {
+ display: none;
+ }
+ i.trigger {
+ padding: 22px 12px;
+ }
+ .logo {
+ position: relative;
+ padding-right: 12px;
+ padding-left: 12px;
+ }
+ .right {
+ position: absolute;
+ top: 0;
+ right: 12px;
+ background: #fff;
+ .account {
+ .avatar {
+ margin-right: 0;
+ }
+ }
+ }
+}
diff --git a/thain-fe/src/components/HeaderDropdown/index.less b/thain-fe/src/components/HeaderDropdown/index.less
new file mode 100644
index 00000000..854d744d
--- /dev/null
+++ b/thain-fe/src/components/HeaderDropdown/index.less
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+@import '~antd/lib/style/themes/default.less';
+
+.container > * {
+ background-color: #fff;
+ border-radius: 4px;
+ box-shadow: @shadow-1-down;
+}
+
+@media screen and (max-width: @screen-xs) {
+ .container {
+ width: 100% !important;
+ }
+ .container > * {
+ border-radius: 0 !important;
+ }
+}
diff --git a/thain-fe/src/components/HeaderDropdown/index.tsx b/thain-fe/src/components/HeaderDropdown/index.tsx
new file mode 100644
index 00000000..09f40547
--- /dev/null
+++ b/thain-fe/src/components/HeaderDropdown/index.tsx
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+import { Dropdown } from 'antd';
+import { DropDownProps } from 'antd/es/dropdown';
+import classNames from 'classnames';
+import styles from './index.less';
+
+declare type OverlayFunc = () => React.ReactNode;
+
+export interface HeaderDropdownProps extends DropDownProps {
+ overlayClassName?: string;
+ overlay: React.ReactNode | OverlayFunc;
+ placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
+}
+
+const HeaderDropdown: React.FC = ({ overlayClassName: cls, ...restProps }) => (
+
+);
+
+export default HeaderDropdown;
diff --git a/thain-fe/src/components/PageHeaderWrapper/index.less b/thain-fe/src/components/PageHeaderWrapper/index.less
new file mode 100644
index 00000000..eb74c2ac
--- /dev/null
+++ b/thain-fe/src/components/PageHeaderWrapper/index.less
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+@import '~antd/lib/style/themes/default.less';
+
+.content {
+ margin: 24px 24px 0;
+}
+
+@media screen and (max-width: @screen-sm) {
+ .content {
+ margin: 24px 0 0;
+ }
+}
diff --git a/thain-fe/src/components/PageHeaderWrapper/index.tsx b/thain-fe/src/components/PageHeaderWrapper/index.tsx
new file mode 100644
index 00000000..0c9cbe51
--- /dev/null
+++ b/thain-fe/src/components/PageHeaderWrapper/index.tsx
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+import { RouteContext } from '@ant-design/pro-layout';
+import { PageHeader, Typography } from 'antd';
+import styles from './index.less';
+
+interface IPageHeaderWrapperProps {
+ content?: React.ReactNode;
+ title: React.ReactNode;
+}
+
+const PageHeaderWrapper: React.SFC = ({
+ children,
+ title,
+ content,
+ ...restProps
+}) => (
+
+ {value => (
+
+
+ {title || value.title}
+
+ }
+ {...restProps}
+ >
+ {content}
+
+ {children ?
{children}
: null}
+
+ )}
+
+);
+
+export default PageHeaderWrapper;
diff --git a/thain-fe/src/components/PageLoading/index.tsx b/thain-fe/src/components/PageLoading/index.tsx
new file mode 100644
index 00000000..03939f84
--- /dev/null
+++ b/thain-fe/src/components/PageLoading/index.tsx
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+import { Spin } from 'antd';
+
+// loading components from code split
+// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
+const PageLoding: React.FC = () => (
+
+
+
+);
+export default PageLoding;
diff --git a/thain-fe/src/components/SelectLang/index.less b/thain-fe/src/components/SelectLang/index.less
new file mode 100644
index 00000000..e475bb99
--- /dev/null
+++ b/thain-fe/src/components/SelectLang/index.less
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+@import '~antd/lib/style/themes/default.less';
+
+.menu {
+ :global(.anticon) {
+ margin-right: 8px;
+ }
+ :global(.ant-dropdown-menu-item) {
+ min-width: 160px;
+ }
+}
+
+.dropDown {
+ line-height: @layout-header-height;
+ vertical-align: top;
+ cursor: pointer;
+ > i {
+ font-size: 16px !important;
+ transform: none !important;
+ svg {
+ position: relative;
+ top: -1px;
+ }
+ }
+}
diff --git a/thain-fe/src/components/SelectLang/index.tsx b/thain-fe/src/components/SelectLang/index.tsx
new file mode 100644
index 00000000..a2245050
--- /dev/null
+++ b/thain-fe/src/components/SelectLang/index.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+import { formatMessage, setLocale, getLocale } from 'umi-plugin-react/locale';
+import { Menu, Icon } from 'antd';
+import { ClickParam } from 'antd/es/menu';
+import classNames from 'classnames';
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+interface SelectLangProps {
+ className?: string;
+}
+const SelectLang: React.FC = props => {
+ const { className } = props;
+ const selectedLang = getLocale();
+ const changeLang = ({ key }: ClickParam) => setLocale(key, true);
+ const locales = ['zh-CN', 'en-US'];
+ const languageLabels = {
+ 'zh-CN': '简体中文',
+ 'en-US': 'English',
+ };
+ const languageIcons = {
+ 'zh-CN': '🇨🇳',
+ 'en-US': '🇬🇧',
+ };
+ const langMenu = (
+
+ );
+ return (
+
+
+
+
+
+ );
+};
+
+export default SelectLang;
diff --git a/thain-fe/src/enums/FlowExecutionStatus.ts b/thain-fe/src/enums/FlowExecutionStatus.ts
new file mode 100644
index 00000000..88ce320c
--- /dev/null
+++ b/thain-fe/src/enums/FlowExecutionStatus.ts
@@ -0,0 +1,41 @@
+import { formatMessage } from 'umi-plugin-react/locale';
+
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+/**
+ * flowExecution的执行状态
+ *
+ * @author liangyongrui
+ */
+export enum FlowExecutionStatus {
+ /**
+ * 1 正在运行
+ */
+ RUNNING = 1,
+ /**
+ * 2 执行成功
+ */
+ SUCCESS = 2,
+ /**
+ * 3 执行异常
+ */
+ ERROR = 3,
+
+ /**
+ * 4 KILLED
+ */
+ KILLED = 4,
+}
+const map = {
+ [FlowExecutionStatus.RUNNING]: formatMessage({ id: 'flow.execution.running' }),
+ [FlowExecutionStatus.SUCCESS]: formatMessage({ id: 'flow.execution.success' }),
+ [FlowExecutionStatus.ERROR]: formatMessage({ id: 'flow.execution.error' }),
+ [FlowExecutionStatus.KILLED]: formatMessage({ id: 'flow.execution.killed' }),
+};
+
+export function getScheduleStatusDesc(enumStatus: FlowExecutionStatus) {
+ return map[enumStatus];
+}
diff --git a/thain-fe/src/enums/FlowLastRunStatus.ts b/thain-fe/src/enums/FlowLastRunStatus.ts
new file mode 100644
index 00000000..bb7196d2
--- /dev/null
+++ b/thain-fe/src/enums/FlowLastRunStatus.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export enum FlowLastRunStatus {
+ /**
+ * 1 未运行
+ */
+ NEVER = 1,
+ /**
+ * 2 运行成功
+ */
+ SUCCESS = 2,
+ /**
+ * 3 运行异常
+ */
+ ERROR = 3,
+ /**
+ * 4 正在运行
+ */
+ RUNNING = 4,
+ /**
+ * 5 手动杀死
+ */
+ KILLED = 5,
+ /**
+ * 6 暂停运行
+ */
+ PAUSE = 6,
+}
+let entries: [string, number][];
+export function FlowLastRunStatusGetEntries() {
+ if (entries === undefined) {
+ entries = [];
+ for (const enumMember of Object.keys(FlowLastRunStatus)) {
+ const value = parseInt(enumMember, 10);
+ if (value > 0) {
+ entries.push([FlowLastRunStatus[enumMember], value]);
+ }
+ }
+ }
+ return entries;
+}
diff --git a/thain-fe/src/enums/FlowSchedulingStatus.ts b/thain-fe/src/enums/FlowSchedulingStatus.ts
new file mode 100644
index 00000000..7a8b7819
--- /dev/null
+++ b/thain-fe/src/enums/FlowSchedulingStatus.ts
@@ -0,0 +1,58 @@
+import { formatMessage } from 'umi-plugin-react/locale';
+
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+/**
+ * flow的调度状态
+ *
+ * @author liangyongrui
+ */
+export enum FlowSchedulingStatus {
+ /**
+ * 1 调度中
+ */
+ SCHEDULING = 1,
+ /**
+ * 2 暂停调度
+ */
+ PAUSE = 2,
+
+ /**
+ * 3 未设置调度(只运行一次的任务)
+ */
+ NOT_SET = 3,
+}
+const codeMap = new Map([
+ [FlowSchedulingStatus.SCHEDULING, formatMessage({ id: 'flow.schedule.scheduling' })],
+ [FlowSchedulingStatus.PAUSE, formatMessage({ id: 'flow.schedule.pause' })],
+ [FlowSchedulingStatus.NOT_SET, formatMessage({ id: 'flow.schedule.not.set' })],
+]);
+
+export function getScheduleStatusDesc(enumCode: FlowSchedulingStatus) {
+ return codeMap.get(enumCode);
+}
+const descMap = new Map([
+ [formatMessage({ id: 'flow.schedule.scheduling' }), FlowSchedulingStatus.SCHEDULING],
+ [formatMessage({ id: 'flow.schedule.pause' }), FlowSchedulingStatus.PAUSE],
+ [formatMessage({ id: 'flow.schedule.not.set' }), FlowSchedulingStatus.NOT_SET],
+]);
+
+export function getScheduleStatusCode(enumDesc: string) {
+ return descMap.get(enumDesc) || 0;
+}
+let entries: [string, number][];
+export function FlowSchedulingStatusGetEntries() {
+ if (entries === undefined) {
+ entries = [];
+ for (const enumMember of Object.keys(FlowSchedulingStatus)) {
+ const value = parseInt(enumMember, 10);
+ if (value > 0) {
+ entries.push([FlowSchedulingStatus[enumMember], value]);
+ }
+ }
+ }
+ return entries;
+}
diff --git a/thain-fe/src/enums/JobExecutionStatus.ts b/thain-fe/src/enums/JobExecutionStatus.ts
new file mode 100644
index 00000000..b34e02f5
--- /dev/null
+++ b/thain-fe/src/enums/JobExecutionStatus.ts
@@ -0,0 +1,41 @@
+import { formatMessage } from 'umi-plugin-react/locale';
+
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+/**
+ * JobExecution的执行状态
+ *
+ * @author liangyongrui
+ */
+export enum JobExecutionStatus {
+ /**
+ * 1 未运行
+ */
+ NEVER = 1,
+ /**
+ * 2 正在运行
+ */
+ RUNNING = 2,
+ /**
+ * 3 执行成功
+ */
+ SUCCESS = 3,
+ /**
+ * 4 执行异常
+ */
+ ERROR = 4,
+}
+
+const map = {
+ [JobExecutionStatus.NEVER]: formatMessage({ id: 'job.execution.never' }),
+ [JobExecutionStatus.RUNNING]: formatMessage({ id: 'job.execution.running' }),
+ [JobExecutionStatus.SUCCESS]: formatMessage({ id: 'job.execution.success' }),
+ [JobExecutionStatus.ERROR]: formatMessage({ id: 'job.execution.error' }),
+};
+
+export function getScheduleStatusDesc(enumStatus: JobExecutionStatus) {
+ return map[enumStatus];
+}
diff --git a/thain-fe/src/global.less b/thain-fe/src/global.less
new file mode 100644
index 00000000..c36b4341
--- /dev/null
+++ b/thain-fe/src/global.less
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+@import '~antd/lib/style/themes/default.less';
+
+html,
+body,
+#root {
+ height: 100%;
+}
+
+.colorWeak {
+ filter: invert(80%);
+}
+
+.ant-layout {
+ min-height: 100vh;
+}
+
+canvas {
+ display: block;
+}
+
+body {
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+ul,
+ol {
+ list-style: none;
+}
+
+@media (max-width: @screen-xs) {
+ .ant-table {
+ width: 100%;
+ overflow-x: auto;
+ &-thead > tr,
+ &-tbody > tr {
+ > th,
+ > td {
+ white-space: pre;
+ > span {
+ display: block;
+ }
+ }
+ }
+ }
+}
diff --git a/thain-fe/src/global.tsx b/thain-fe/src/global.tsx
new file mode 100644
index 00000000..39d2d2b7
--- /dev/null
+++ b/thain-fe/src/global.tsx
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+import { notification, Button, message } from 'antd';
+import { formatMessage } from 'umi-plugin-react/locale';
+import defaultSettings from '../config/defaultSettings';
+
+const { pwa } = defaultSettings;
+// if pwa is true
+if (pwa) {
+ // Notify user if offline now
+ window.addEventListener('sw.offline', () => {
+ message.warning(formatMessage({ id: 'app.pwa.offline' }));
+ });
+
+ // Pop up a prompt on the page asking the user if they want to use the latest version
+ window.addEventListener('sw.updated', (event: Event) => {
+ const e = event as CustomEvent;
+ const reloadSW = async () => {
+ // Check if there is sw whose state is waiting in ServiceWorkerRegistration
+ // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
+ const worker = e.detail && e.detail.waiting;
+ if (!worker) {
+ return Promise.resolve();
+ }
+ // Send skip-waiting event to waiting SW with MessageChannel
+ await new Promise((resolve, reject) => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = msgEvent => {
+ if (msgEvent.data.error) {
+ reject(msgEvent.data.error);
+ } else {
+ resolve(msgEvent.data);
+ }
+ };
+ worker.postMessage({ type: 'skip-waiting' }, [channel.port2]);
+ });
+ // Refresh current page to use the updated HTML and other assets after SW has skiped waiting
+ window.location.reload(true);
+ return true;
+ };
+ const key = `open${Date.now()}`;
+ const btn = (
+
+ );
+ notification.open({
+ message: formatMessage({ id: 'app.pwa.serviceworker.updated' }),
+ description: formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }),
+ btn,
+ key,
+ onClose: async () => {},
+ });
+ });
+} else if ('serviceWorker' in navigator) {
+ // eslint-disable-next-line compat/compat
+ navigator.serviceWorker.ready.then(registration => {
+ registration.unregister();
+ });
+}
diff --git a/thain-fe/src/layouts/BasicLayout.tsx b/thain-fe/src/layouts/BasicLayout.tsx
new file mode 100644
index 00000000..dd1ed156
--- /dev/null
+++ b/thain-fe/src/layouts/BasicLayout.tsx
@@ -0,0 +1,108 @@
+/**
+ * Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout.
+ * You can view component api by:
+ * https://github.com/ant-design/ant-design-pro-layout
+ */
+
+import { ConnectProps, ConnectState } from '@/models/connect';
+import ProLayout, {
+ MenuDataItem,
+ BasicLayoutProps as ProLayoutProps,
+ Settings,
+} from '@ant-design/pro-layout';
+import React, { useState } from 'react';
+
+import Authorized from '@/utils/Authorized';
+import Link from 'umi/link';
+import RightContent from '@/components/GlobalHeader/RightContent';
+import { connect } from 'dva';
+import { formatMessage } from 'umi-plugin-react/locale';
+import logo from '../assets/xdata_logo.png';
+import GlobalFooter from '@/components/GlobalFooter';
+
+export interface BasicLayoutProps extends ProLayoutProps, Omit {
+ breadcrumbNameMap: {
+ [path: string]: MenuDataItem;
+ };
+ settings: Settings;
+}
+export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
+ breadcrumbNameMap: {
+ [path: string]: MenuDataItem;
+ };
+};
+
+/**
+ * use Authorized check all menu item
+ */
+const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] =>
+ menuList.map(item => {
+ const localItem = {
+ ...item,
+ children: item.children ? menuDataRender(item.children) : [],
+ };
+ return Authorized.check(item.authority, localItem, null) as MenuDataItem;
+ });
+
+const BasicLayout: React.FC = props => {
+ const { dispatch, children, settings } = props;
+ /**
+ * constructor
+ */
+
+ useState(() => {
+ if (dispatch) {
+ dispatch({
+ type: 'user/fetchCurrent',
+ });
+ dispatch({
+ type: 'settings/getSetting',
+ });
+ }
+ });
+
+ /**
+ * init variables
+ */
+ const handleMenuCollapse = (payload: boolean): void =>
+ dispatch &&
+ dispatch({
+ type: 'global/changeLayoutCollapsed',
+ payload,
+ });
+
+ return (
+ (
+ {defaultDom}
+ )}
+ breadcrumbRender={(routers = []) => [
+ {
+ path: '/',
+ breadcrumbName: formatMessage({
+ id: 'menu.home',
+ defaultMessage: 'Home',
+ }),
+ },
+ ...routers,
+ ]}
+ footerRender={(a, b) => (
+
+ )}
+ menuDataRender={menuDataRender}
+ formatMessage={formatMessage}
+ rightContentRender={rightProps => }
+ {...props}
+ {...settings}
+ >
+ {children}
+
+ );
+};
+
+export default connect(({ global, settings }: ConnectState) => ({
+ collapsed: global.collapsed,
+ settings,
+}))(BasicLayout);
diff --git a/thain-fe/src/layouts/BlankLayout.tsx b/thain-fe/src/layouts/BlankLayout.tsx
new file mode 100644
index 00000000..976d9372
--- /dev/null
+++ b/thain-fe/src/layouts/BlankLayout.tsx
@@ -0,0 +1,10 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+
+const Layout: React.FC = ({ children }) => {children}
;
+
+export default Layout;
diff --git a/thain-fe/src/layouts/UserLayout.less b/thain-fe/src/layouts/UserLayout.less
new file mode 100644
index 00000000..46d95124
--- /dev/null
+++ b/thain-fe/src/layouts/UserLayout.less
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+@import '~antd/lib/style/themes/default.less';
+
+.container {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ overflow: auto;
+ background: @layout-body-background;
+}
+
+.lang {
+ width: 100%;
+ height: 40px;
+ line-height: 44px;
+ text-align: right;
+ :global(.ant-dropdown-trigger) {
+ margin-right: 24px;
+ }
+}
+
+.content {
+ flex: 1;
+ padding: 32px 0;
+}
+
+@media (min-width: @screen-md-min) {
+ .container {
+ background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
+ background-repeat: no-repeat;
+ background-position: center 110px;
+ background-size: 100%;
+ }
+
+ .content {
+ padding: 32px 0 24px 0;
+ }
+}
+
+.top {
+ text-align: center;
+}
+
+.header {
+ height: 44px;
+ line-height: 44px;
+ a {
+ text-decoration: none;
+ }
+}
+
+.logo {
+ height: 44px;
+ margin-right: 16px;
+ vertical-align: top;
+}
+
+.title {
+ position: relative;
+ top: 2px;
+ color: @heading-color;
+ font-weight: 600;
+ font-size: 33px;
+ font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
+}
+
+.desc {
+ margin-top: 12px;
+ margin-bottom: 40px;
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+}
diff --git a/thain-fe/src/layouts/UserLayout.tsx b/thain-fe/src/layouts/UserLayout.tsx
new file mode 100644
index 00000000..38e6f337
--- /dev/null
+++ b/thain-fe/src/layouts/UserLayout.tsx
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import SelectLang from '@/components/SelectLang';
+import { ConnectProps, ConnectState } from '@/models/connect';
+import { connect } from 'dva';
+import React from 'react';
+import DocumentTitle from 'react-document-title';
+import { formatMessage } from 'umi-plugin-react/locale';
+import Link from 'umi/link';
+import logo from '../assets/xdata_logo.png';
+import styles from './UserLayout.less';
+import { MenuDataItem, getPageTitle, getMenuData } from '@ant-design/pro-layout';
+import GlobalFooter from '@/components/GlobalFooter';
+
+export interface UserLayoutProps extends ConnectProps {
+ breadcrumbNameMap: { [path: string]: MenuDataItem };
+}
+
+const UserLayout: React.SFC = props => {
+ const {
+ route = {
+ routes: [],
+ },
+ } = props;
+ const { routes = [] } = route;
+ const {
+ children,
+ location = {
+ pathname: '',
+ },
+ } = props;
+ const { breadcrumb } = getMenuData(routes);
+
+ return (
+
+
+
+
+
+
+
+
+
+

+
Thain
+
+
+
{formatMessage({ id: 'global.slogan' })}
+
+ {children}
+
+
+
+
+ );
+};
+
+export default connect(({ settings }: ConnectState) => ({
+ ...settings,
+}))(UserLayout);
diff --git a/thain-fe/src/locales/en-US.ts b/thain-fe/src/locales/en-US.ts
new file mode 100644
index 00000000..8518dcce
--- /dev/null
+++ b/thain-fe/src/locales/en-US.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import global from './en-US/global';
+import httpCode from './en-US/httpCode';
+import flow from './en-US/flow';
+import flowExecution from './en-US/flowExecution';
+import dashboard from './en-US/dashboard';
+import menu from './en-US/menu';
+import userLogin from '@/pages/User/Login/locales/en-US';
+import exception403 from '@/pages/Exception/403/locales/en-US';
+import exception404 from '@/pages/Exception/404/locales/en-US';
+import exception500 from '@/pages/Exception/500/locales/en-US';
+import admin from './en-US/admin';
+export default {
+ ...global,
+ ...flow,
+ ...flowExecution,
+ ...dashboard,
+ ...httpCode,
+ ...menu,
+ ...userLogin,
+ ...exception403,
+ ...exception404,
+ ...exception500,
+ ...admin,
+};
diff --git a/thain-fe/src/locales/en-US/admin.ts b/thain-fe/src/locales/en-US/admin.ts
new file mode 100644
index 00000000..ae53f988
--- /dev/null
+++ b/thain-fe/src/locales/en-US/admin.ts
@@ -0,0 +1,18 @@
+export default {
+ 'admin.user.addUser': 'Add User',
+ 'admin.user.userId': 'UserId',
+ 'admin.user.userName': 'UserName',
+ 'admin.user.email': 'Email',
+ 'admin.user.operation': 'Operation',
+ 'admin.user.delete': 'Delete',
+ 'admin.user.password': 'Password',
+ 'admin.user.admin.yes': 'Yes',
+ 'admin.user.admin.no': 'No',
+ 'admin.user.cancel': 'Cancel',
+ 'admin.user.comfirm': 'Confirm',
+ 'admin.user.admin': 'Admin',
+ 'admin.index.userAdmin': 'User Management',
+ 'admin.index.ClientAdmin': 'Client Management',
+ 'admin.user.info': 'UserInfo',
+ 'admin.home.index': 'Admin List',
+};
diff --git a/thain-fe/src/locales/en-US/dashboard.ts b/thain-fe/src/locales/en-US/dashboard.ts
new file mode 100644
index 00000000..cc2b12e7
--- /dev/null
+++ b/thain-fe/src/locales/en-US/dashboard.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ 'common.total': 'Total',
+ 'common.statistical.period': 'Statistical Period',
+ 'common.polygon.max.num': 'Polygon max num',
+ 'common.success': 'Success',
+ 'common.failed': 'Failed',
+ 'flow.execution.status.chart.title': 'Flow running status',
+ 'flow.source.chart.title': 'Flow statistics of accessed systems',
+ 'flow.increase.chart.title': 'Increased Flow',
+ 'flow.running.chart.title': 'Running Flow',
+ 'flow.schedule.status.chart.title': 'Flow scheduling status statistics',
+ 'job.execution.status.chart.title': 'Job running status',
+ 'job.execution.history.chart.title': 'Historical Job running status',
+ 'job.increase.chart.title': 'Increased Job',
+ 'job.running.chart.title': 'Running Job',
+};
diff --git a/thain-fe/src/locales/en-US/flow.ts b/thain-fe/src/locales/en-US/flow.ts
new file mode 100644
index 00000000..15150b02
--- /dev/null
+++ b/thain-fe/src/locales/en-US/flow.ts
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ 'flow.schedule.scheduling': 'Scheduling',
+ 'flow.schedule.pause': 'Paused',
+ 'flow.schedule.not.set': 'Not set',
+ 'flow.set.schedule': 'Setup scheduling',
+ 'flow.begin.schedule': 'Begin scheduling',
+ 'flow.pause.schedule': 'Pause scheduling',
+ 'flow.name': 'Flow name',
+ 'flow.app': 'App',
+ 'flow.cron': 'Cron',
+ 'flow.node': 'Flow node',
+ 'flow.component': 'Component',
+ 'flow.node.name': 'Node name',
+ 'flow.status.callback.url': 'Node status callback address',
+ 'flow.condition': 'Trigger condition',
+ 'flow.last.status': 'Last run status',
+ 'flow.schedule.status': 'Scheduling status',
+ 'flow.status.update.time': 'State update time',
+ 'flow.create.user': 'Create user',
+ 'flow.operation': 'Operator',
+ 'flow.fire': 'Fire',
+ 'flow.view.log': 'View log',
+ 'flow.create': 'Create',
+ 'flow.edit': 'Edit',
+ 'flow.save': 'Save',
+ 'flow.delete': 'Delete',
+ 'flow.cancel': 'cancel',
+ 'flow.retrieve': 'Retrieve',
+ 'flow.delete.tips': 'Are you sure you want to delete it?',
+ 'flow.batch.operation': 'Batch operation',
+ 'flow.batch.fire': 'Batch fire',
+ 'flow.batch.begin': 'Batch scheduling',
+ 'flow.batch.pause': 'Batch pause',
+ 'flow.batch.delete': 'Batch delete',
+ 'flow.management': 'Flow management',
+ 'flow.begin.schedule.success': 'Begin Scheduling success',
+ 'flow.pause.schedule.success': 'Pause Scheduling success',
+ 'flow.fire.success': 'Fire success',
+ 'flow.delete.success': 'Delete success',
+ 'flow.last.running.status': 'Last run status',
+ 'flow.node.edit': 'Edit node',
+ 'flow.node.empty': 'Nodes are empty',
+ 'flow.create.success': 'Save success',
+ 'flow.save.confirm': 'Please confirm the information of each node',
+ 'flow.save.cancel': 'Check again',
+ 'flow.kill.success': 'Kill success',
+ 'flow.grid.align': 'Grid align',
+ 'flow.failure.alarm.mail': 'Failure alarm mail',
+ 'flow.autokill.settings': 'SLA settings',
+ 'flow.duration': 'Duration',
+};
diff --git a/thain-fe/src/locales/en-US/flowExecution.ts b/thain-fe/src/locales/en-US/flowExecution.ts
new file mode 100644
index 00000000..4fe303c5
--- /dev/null
+++ b/thain-fe/src/locales/en-US/flowExecution.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ 'flow.execution.running': 'Running',
+ 'flow.execution.success': 'Success',
+ 'flow.execution.error': 'Error',
+ 'flow.execution.killed': 'Killed',
+ 'flow.execution.consuming': 'Time consuming',
+ 'flow.execution.view.log': 'View log',
+ 'flow.execution.trigger.type': 'Trigger type',
+ 'flow.execution.manual': 'Manual',
+ 'flow.execution.auto': 'Auto',
+ 'flow.execution.status': 'Flow execution status',
+ 'flow.execution.execution.machine': 'Execution machine',
+ 'flow.execution.create.time': 'Create time',
+ 'flow.execution.update.time': 'Update time',
+ 'flow.execution.operation': 'Operation',
+ 'flow.execution.log.detail': 'Log details',
+ 'job.execution.running': 'Running',
+ 'job.execution.success': 'Success',
+ 'job.execution.error': 'Error',
+ 'job.execution.never': 'Never',
+};
diff --git a/thain-fe/src/locales/en-US/global.ts b/thain-fe/src/locales/en-US/global.ts
new file mode 100644
index 00000000..b76ebe3d
--- /dev/null
+++ b/thain-fe/src/locales/en-US/global.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ 'global.help': 'Documents',
+ 'global.lang': 'Language',
+ 'global.copyright': 'Xiaomi',
+ 'global.slogan': 'Reliable, Easy to Use and Customizable Open Source Scheduling System',
+ 'global.deny.messgae': 'Sorry, you have no access to this page.',
+ 'global.absent.message': 'Sorry, the page you visited does not exist.',
+ 'global.server.error.message': 'Sorry, the server is wrong.',
+ 'global.input.placeholder': 'Please enter',
+ 'global.select.placeholder': 'Please select',
+ 'global.navigation': 'Navigation',
+ 'global.undo': 'Undo',
+ 'global.redo': 'Redo',
+ 'global.copy': 'Copy',
+ 'global.paste': 'Paste',
+ 'global.delete': 'Delete',
+ 'global.zoom.in': 'Zoom in',
+ 'global.zoom.out': 'Zoom out',
+ 'global.auto.zoom': 'Auto zoom',
+ 'global.reset.zoom': 'Reset zoom',
+ 'global.to.back': 'Hierarchical poststack',
+ 'global.to.front': 'Hierarchical Preposition',
+ 'global.multi.select': 'Multiple select',
+ 'global.tagSelect.expand': 'Expand',
+ 'global.tagSelect.collapse': 'Collapse',
+};
diff --git a/thain-fe/src/locales/en-US/httpCode.ts b/thain-fe/src/locales/en-US/httpCode.ts
new file mode 100644
index 00000000..c2066732
--- /dev/null
+++ b/thain-fe/src/locales/en-US/httpCode.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ '200': 'The server successfully returned the requested data.',
+ '201': 'Successful data creation or modification.',
+ '202': 'A request has entered the background queue (asynchronous task).',
+ '204': 'Data deletion was successful.',
+ '400': 'There was an error in the request, and the server did not create or modify the data.',
+ '401': 'Users do not have permission (token, username or password error).',
+ '403': 'Users are authorized, but access is prohibited.',
+ '404': 'The request is for non-existent records, and the server is not operating.',
+ '406': 'The format of the request is not available.',
+ '410': 'The requested resource is permanently deleted and will not be retrieved.',
+ '422': 'When an object is created, a validation error occurs.',
+ '500': 'Server error, please check the server.',
+ '502': 'Gateway error.',
+ '503': 'The service is unavailable and the server is temporarily overloaded or maintained.',
+ '504': 'Gateway timeout.',
+};
diff --git a/thain-fe/src/locales/en-US/menu.ts b/thain-fe/src/locales/en-US/menu.ts
new file mode 100644
index 00000000..997a3d05
--- /dev/null
+++ b/thain-fe/src/locales/en-US/menu.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ 'menu.executions': 'Executions',
+ 'menu.editor': 'New/Editor',
+ 'menu.flows': 'Flows',
+ 'menu.dashboard': 'Dashboard',
+ 'menu.home': 'Home',
+ 'menu.login': 'Login',
+ 'menu.exception.403': '403',
+ 'menu.exception.404': '404',
+ 'menu.exception.500': '500',
+ 'menu.account.center': 'User Center',
+ 'menu.account.settings': 'User Setting',
+ 'menu.account.logout': 'Logout',
+ 'menu.admin': 'Admin',
+};
diff --git a/thain-fe/src/locales/zh-CN.ts b/thain-fe/src/locales/zh-CN.ts
new file mode 100644
index 00000000..a0464955
--- /dev/null
+++ b/thain-fe/src/locales/zh-CN.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import global from './zh-CN/global';
+import httpCode from './zh-CN/httpCode';
+import flow from './zh-CN/flow';
+import flowExecution from './zh-CN/flowExecution';
+import dashboard from './zh-CN/dashboard';
+import menu from './zh-CN/menu';
+import userLogin from '@/pages/User/Login/locales/zh-CN';
+import exception403 from '@/pages/Exception/403/locales/zh-CN';
+import exception404 from '@/pages/Exception/404/locales/zh-CN';
+import exception500 from '@/pages/Exception/500/locales/zh-CN';
+import admin from './zh-CN/admin';
+export default {
+ ...global,
+ ...flow,
+ ...flowExecution,
+ ...dashboard,
+ ...httpCode,
+ ...menu,
+ ...userLogin,
+ ...exception403,
+ ...exception404,
+ ...exception500,
+ ...admin,
+};
diff --git a/thain-fe/src/locales/zh-CN/admin.ts b/thain-fe/src/locales/zh-CN/admin.ts
new file mode 100644
index 00000000..246c4955
--- /dev/null
+++ b/thain-fe/src/locales/zh-CN/admin.ts
@@ -0,0 +1,18 @@
+export default {
+ 'admin.user.addUser': '添加用户',
+ 'admin.user.userId': '用户ID',
+ 'admin.user.userName': '用户名',
+ 'admin.user.email': '电子邮箱',
+ 'admin.user.operation': '操作',
+ 'admin.user.delete': '删除',
+ 'admin.user.password': '密码',
+ 'admin.user.admin.yes': '是',
+ 'admin.user.admin.no': '否',
+ 'admin.user.cancel': '确认',
+ 'admin.user.comfirm': '取消',
+ 'admin.user.admin': '管理员',
+ 'admin.index.userAdmin': '用户管理',
+ 'admin.index.ClientAdmin': '客户端管理',
+ 'admin.user.info': '用户信息',
+ 'admin.home.index': '管理列表',
+};
diff --git a/thain-fe/src/locales/zh-CN/dashboard.ts b/thain-fe/src/locales/zh-CN/dashboard.ts
new file mode 100644
index 00000000..251fe856
--- /dev/null
+++ b/thain-fe/src/locales/zh-CN/dashboard.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ 'common.total': '总数',
+ 'common.statistical.period': '统计时段',
+ 'common.polygon.max.num': '折线图最大显示点数',
+ 'common.success': '成功',
+ 'common.failed': '失败',
+ 'flow.execution.status.chart.title': 'Flow Execution运行情况',
+ 'flow.source.chart.title': '已接入系统任务统计',
+ 'flow.increase.chart.title': '新增Flow',
+ 'flow.running.chart.title': '运行中flow',
+ 'flow.schedule.status.chart.title': 'Flow调度状态统计',
+ 'job.execution.status.chart.title': 'Job Execution运行情况',
+ 'job.execution.history.chart.title': '历史任务执行情况',
+ 'job.increase.chart.title': '新增Job',
+ 'job.running.chart.title': '运行中Job',
+};
diff --git a/thain-fe/src/locales/zh-CN/flow.ts b/thain-fe/src/locales/zh-CN/flow.ts
new file mode 100644
index 00000000..70e243b5
--- /dev/null
+++ b/thain-fe/src/locales/zh-CN/flow.ts
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ 'flow.schedule.scheduling': '调度中',
+ 'flow.schedule.pause': '暂停调度',
+ 'flow.schedule.not.set': '未设置调度',
+ 'flow.set.schedule': '设置调度',
+ 'flow.begin.schedule': '开始调度',
+ 'flow.pause.schedule': '暂停调度',
+ 'flow.name': 'Flow名称',
+ 'flow.app': 'App',
+ 'flow.cron': 'Cron',
+ 'flow.node': '流程节点',
+ 'flow.component': '组件',
+ 'flow.node.name': '节点名称',
+ 'flow.status.callback.url': '状态回调地址',
+ 'flow.condition': '触发条件',
+ 'flow.last.status': '最后一次运行状态',
+ 'flow.schedule.status': '调度状态',
+ 'flow.status.update.time': '状态更新时间',
+ 'flow.create.user': '创建人',
+ 'flow.operation': '操作',
+ 'flow.fire': '立即执行',
+ 'flow.view.log': '查看日志',
+ 'flow.create': '新建',
+ 'flow.edit': '编辑',
+ 'flow.save': '保存',
+ 'flow.delete': '删除',
+ 'flow.cancel': '取消',
+ 'flow.retrieve': '查询',
+ 'flow.delete.tips': '确定要删除吗',
+ 'flow.batch.operation': '批量操作',
+ 'flow.batch.fire': '批量执行',
+ 'flow.batch.begin': '批量开始',
+ 'flow.batch.pause': '批量暂停',
+ 'flow.batch.delete': '批量删除',
+ 'flow.management': 'Flow 管理',
+ 'flow.begin.schedule.success': '开始调度成功',
+ 'flow.pause.schedule.success': '暂停调度成功',
+ 'flow.fire.success': '执行成功',
+ 'flow.delete.success': '删除成功',
+ 'flow.last.running.status': '最后一次运行状态',
+ 'flow.node.edit': '编辑节点',
+ 'flow.node.empty': '节点为空',
+ 'flow.create.success': '保存成功',
+ 'flow.save.confirm': '请确认每个节点的信息',
+ 'flow.save.cancel': '我再看看',
+ 'flow.kill.success': '杀死成功',
+ 'flow.grid.align': '网格对齐',
+ 'flow.failure.alarm.mail': '失败报警邮箱',
+ 'flow.autokill.settings': 'SLA设置',
+ 'flow.duration': '持续时间',
+};
diff --git a/thain-fe/src/locales/zh-CN/flowExecution.ts b/thain-fe/src/locales/zh-CN/flowExecution.ts
new file mode 100644
index 00000000..74878047
--- /dev/null
+++ b/thain-fe/src/locales/zh-CN/flowExecution.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ 'flow.execution.running': '正在运行',
+ 'flow.execution.success': '执行成功',
+ 'flow.execution.error': '执行异常',
+ 'flow.execution.killed': '手动杀死',
+ 'flow.execution.consuming': '耗时',
+ 'flow.execution.view.log': '查看日志',
+ 'flow.execution.trigger.type': '触发类型',
+ 'flow.execution.manual': '手动',
+ 'flow.execution.auto': '自动',
+ 'flow.execution.execution.machine': '执行机器',
+ 'flow.execution.create.time': '创建时间',
+ 'flow.execution.update.time': '更新时间',
+ 'flow.execution.status': '作业状态',
+ 'flow.execution.operation': '操作',
+ 'flow.execution.log.detail': '日志详情',
+ 'job.execution.running': '正在运行',
+ 'job.execution.success': '执行成功',
+ 'job.execution.error': '执行异常',
+ 'job.execution.never': '未运行',
+};
diff --git a/thain-fe/src/locales/zh-CN/global.ts b/thain-fe/src/locales/zh-CN/global.ts
new file mode 100644
index 00000000..9a77c19b
--- /dev/null
+++ b/thain-fe/src/locales/zh-CN/global.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ 'global.help': '使用文档',
+ 'global.lang': '语言',
+ 'global.copyright': '小米',
+ 'global.slogan': '可靠、易用、可定制的开源调度系统',
+ 'global.deny.messgae': '抱歉,你无权访问该页面',
+ 'global.absent.message': '抱歉,你访问的页面不存在',
+ 'global.server.error.message': '抱歉,服务器出错了',
+ 'global.input.placeholder': '请输入',
+ 'global.select.placeholder': '请选择',
+ 'global.navigation': '导航器',
+ 'global.undo': '撤销',
+ 'global.redo': '重做',
+ 'global.copy': '复制',
+ 'global.paste': '粘贴',
+ 'global.delete': '删除',
+ 'global.zoom.in': '放大',
+ 'global.zoom.out': '缩小',
+ 'global.auto.zoom': '适应画布',
+ 'global.reset.zoom': '实际尺寸',
+ 'global.to.back': '层级后叠',
+ 'global.to.front': '层级前置',
+ 'global.multi.select': '多选',
+ 'global.tagSelect.expand': '展开',
+ 'global.tagSelect.collapse': '收起',
+};
diff --git a/thain-fe/src/locales/zh-CN/httpCode.ts b/thain-fe/src/locales/zh-CN/httpCode.ts
new file mode 100644
index 00000000..12abbd56
--- /dev/null
+++ b/thain-fe/src/locales/zh-CN/httpCode.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ '200': '服务器成功返回请求的数据。',
+ '201': '新建或修改数据成功。',
+ '202': '一个请求已经进入后台排队(异步任务)。',
+ '204': '删除数据成功。',
+ '400': '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
+ '401': '用户没有权限(令牌、用户名、密码错误)。',
+ '403': '用户得到授权,但是访问是被禁止的。',
+ '404': '发出的请求针对的是不存在的记录,服务器没有进行操作。',
+ '406': '请求的格式不可得。',
+ '410': '请求的资源被永久删除,且不会再得到的。',
+ '422': '当创建一个对象时,发生一个验证错误。',
+ '500': '服务器发生错误,请检查服务器。',
+ '502': '网关错误。',
+ '503': '服务不可用,服务器暂时过载或维护。',
+ '504': '网关超时。',
+};
diff --git a/thain-fe/src/locales/zh-CN/menu.ts b/thain-fe/src/locales/zh-CN/menu.ts
new file mode 100644
index 00000000..6a795afc
--- /dev/null
+++ b/thain-fe/src/locales/zh-CN/menu.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ 'menu.executions': '执行列表',
+ 'menu.editor': '新建/编辑',
+ 'menu.flows': '流程列表',
+ 'menu.dashboard': '仪表盘',
+ 'menu.home': '首页',
+ 'menu.login': '登录',
+ 'menu.exception.403': '403',
+ 'menu.exception.404': '404',
+ 'menu.exception.500': '500',
+ 'menu.account.center': '个人中心',
+ 'menu.account.settings': '个人设置',
+ 'menu.account.logout': '退出登录',
+ 'menu.admin': '管理列表',
+};
diff --git a/thain-fe/src/manifest.json b/thain-fe/src/manifest.json
new file mode 100644
index 00000000..09a53325
--- /dev/null
+++ b/thain-fe/src/manifest.json
@@ -0,0 +1,22 @@
+{
+ "name": "Thain",
+ "short_name": "Thain",
+ "display": "standalone",
+ "start_url": "./?utm_source=homescreen",
+ "theme_color": "#002140",
+ "background_color": "#001529",
+ "icons": [
+ {
+ "src": "icons/icon-192x192.png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "icons/icon-128x128.png",
+ "sizes": "128x128"
+ },
+ {
+ "src": "icons/icon-512x512.png",
+ "sizes": "512x512"
+ }
+ ]
+}
diff --git a/thain-fe/src/models/connect.d.ts b/thain-fe/src/models/connect.d.ts
new file mode 100644
index 00000000..b1cf0983
--- /dev/null
+++ b/thain-fe/src/models/connect.d.ts
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { EffectsCommandMap } from 'dva';
+import { AnyAction } from 'redux';
+import { RouterTypes } from 'umi';
+import { GlobalModelState } from './global';
+import { UserModelState } from './user';
+import { DefaultSettings as SettingModelState } from '../../config/defaultSettings';
+import { MenuDataItem } from '@ant-design/pro-layout';
+import { FlowExecutionListModelState } from '../pages/FlowExecution/List/models/flowExecutionList';
+import { FlowEditorModelState } from '../pages/FlowEditor/model';
+import { FlowListModelState } from '@/pages/Flow/List/model';
+import { FlowExecutionDetailModelState } from '@/pages/FlowExecution/Detail/model';
+import { DashboardState } from '@/pages/Dashboard/model';
+import { AdminUserModel } from '@/pages/admin/model';
+export { GlobalModelState, SettingModelState, UserModelState };
+
+export type Effect = (
+ action: AnyAction,
+ effects: EffectsCommandMap & { select: (func: (state: ConnectState) => T) => T },
+) => void;
+
+/**
+ * @type P: Type of payload
+ * @type C: Type of callback
+ */
+export type Dispatch = void>(action: {
+ type: string;
+ payload?: P;
+ callback?: C;
+ [key: string]: any;
+}) => any;
+
+export interface Loading {
+ global: boolean;
+ effects: { [key: string]: boolean | undefined };
+ models: {
+ global?: boolean;
+ menu?: boolean;
+ setting?: boolean;
+ user?: boolean;
+ flowExecutionList?: boolean;
+ flowExecutionDetail?: boolean;
+ flowEditor?: boolean;
+ flowList?: boolean;
+ admin?: boolean;
+ };
+}
+
+/**
+ * key 为models下面的namespace
+ */
+export interface ConnectState {
+ global: GlobalModelState;
+ loading: Loading;
+ settings: SettingModelState;
+ user: UserModelState;
+ flowExecutionList: FlowExecutionListModelState;
+ flowExecutionDetail: FlowExecutionDetailModelState;
+ flowList: FlowListModelState;
+ flowEditor: FlowEditorModelState;
+ dashboard: DashboardState;
+ admin: AdminUserModel;
+}
+
+export interface Route extends MenuDataItem {
+ routes?: Route[];
+}
+
+/**
+ * @type T: Params matched in dynamic routing
+ */
+export interface ConnectProps
+ extends Partial> {
+ dispatch?: Dispatch;
+}
+
+export default ConnectState;
diff --git a/thain-fe/src/models/global.ts b/thain-fe/src/models/global.ts
new file mode 100644
index 00000000..743678db
--- /dev/null
+++ b/thain-fe/src/models/global.ts
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { queryNotices } from '@/services/user';
+import { Subscription } from 'dva';
+import { Reducer } from 'redux';
+import { Effect } from './connect';
+import { NoticeIconData } from '@/components/NoticeIcon';
+
+export interface NoticeItem extends NoticeIconData {
+ id: string;
+ type: string;
+ [key: string]: any;
+}
+
+export interface GlobalModelState {
+ collapsed: boolean;
+ notices: NoticeItem[];
+}
+
+export interface GlobalModelType {
+ namespace: 'global';
+ state: GlobalModelState;
+ effects: {
+ fetchNotices: Effect;
+ clearNotices: Effect;
+ changeNoticeReadState: Effect;
+ };
+ reducers: {
+ changeLayoutCollapsed: Reducer;
+ saveNotices: Reducer;
+ saveClearedNotices: Reducer;
+ };
+ subscriptions: { setup: Subscription };
+}
+
+const GlobalModel: GlobalModelType = {
+ namespace: 'global',
+
+ state: {
+ collapsed: false,
+ notices: [],
+ },
+
+ effects: {
+ *fetchNotices(_, { call, put, select }) {
+ const data = yield call(queryNotices);
+ yield put({
+ type: 'saveNotices',
+ payload: data,
+ });
+ const unreadCount: number = yield select(
+ state => state.global.notices.filter(item => !item.read).length,
+ );
+ yield put({
+ type: 'user/changeNotifyCount',
+ payload: {
+ totalCount: data.length,
+ unreadCount,
+ },
+ });
+ },
+ *clearNotices({ payload }, { put, select }) {
+ yield put({
+ type: 'saveClearedNotices',
+ payload,
+ });
+ const count: number = yield select(state => state.global.notices.length);
+ const unreadCount: number = yield select(
+ state => state.global.notices.filter(item => !item.read).length,
+ );
+ yield put({
+ type: 'user/changeNotifyCount',
+ payload: {
+ totalCount: count,
+ unreadCount,
+ },
+ });
+ },
+ *changeNoticeReadState({ payload }, { put, select }) {
+ const notices: NoticeItem[] = yield select(state =>
+ state.global.notices.map(item => {
+ const notice = { ...item };
+ if (notice.id === payload) {
+ notice.read = true;
+ }
+ return notice;
+ }),
+ );
+ yield put({
+ type: 'saveNotices',
+ payload: notices,
+ });
+ yield put({
+ type: 'user/changeNotifyCount',
+ payload: {
+ totalCount: notices.length,
+ unreadCount: notices.filter(item => !item.read).length,
+ },
+ });
+ },
+ },
+
+ reducers: {
+ changeLayoutCollapsed(state = { notices: [], collapsed: true }, { payload }) {
+ return {
+ ...state,
+ collapsed: payload,
+ };
+ },
+ saveNotices(state, { payload }) {
+ return {
+ collapsed: false,
+ ...state,
+ notices: payload,
+ };
+ },
+ saveClearedNotices(state = { notices: [], collapsed: true }, { payload }) {
+ return {
+ collapsed: false,
+ ...state,
+ notices: state.notices.filter(item => item.type !== payload),
+ };
+ },
+ },
+
+ subscriptions: {
+ setup({ history }) {
+ // Subscribe history(url) change, trigger `load` action if pathname is `/`
+ return history.listen(({ pathname, search }) => {
+ if (typeof (window as any).ga !== 'undefined') {
+ (window as any).ga('send', 'pageview', pathname + search);
+ }
+ });
+ },
+ },
+};
+
+export default GlobalModel;
diff --git a/thain-fe/src/models/login.ts b/thain-fe/src/models/login.ts
new file mode 100644
index 00000000..d92f26f8
--- /dev/null
+++ b/thain-fe/src/models/login.ts
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { Reducer, AnyAction } from 'redux';
+import { EffectsCommandMap } from 'dva';
+import { parse } from 'qs';
+import { logout } from '@/services/login';
+
+export function getPageQuery() {
+ return parse(window.location.href.split('?')[1]);
+}
+
+export type Effect = (
+ action: AnyAction,
+ effects: EffectsCommandMap & { select: (func: (state: {}) => T) => T },
+) => void;
+
+export interface ModelType {
+ namespace: string;
+ state: {};
+ effects: {
+ logout: Effect;
+ };
+ reducers: {
+ changeLoginStatus: Reducer<{}>;
+ };
+}
+
+const Model: ModelType = {
+ namespace: 'login',
+
+ state: {
+ status: undefined,
+ },
+
+ effects: {
+ *logout(_, { call, put }) {
+ yield call(logout);
+ window.location.reload();
+ },
+ },
+
+ reducers: {
+ changeLoginStatus(state, { payload }) {
+ return {
+ ...state,
+ status: payload.status,
+ type: payload.type,
+ };
+ },
+ },
+};
+
+export default Model;
diff --git a/thain-fe/src/models/setting.ts b/thain-fe/src/models/setting.ts
new file mode 100644
index 00000000..53c35fff
--- /dev/null
+++ b/thain-fe/src/models/setting.ts
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { message } from 'antd';
+import { Reducer } from 'redux';
+import defaultSettings, { DefaultSettings } from '../../config/defaultSettings';
+
+export interface SettingModelType {
+ namespace: 'settings';
+ state: DefaultSettings;
+ reducers: {
+ getSetting: Reducer;
+ changeSetting: Reducer;
+ };
+}
+let lessNodesAppended: boolean;
+
+const updateTheme: (primaryColor?: string) => void = primaryColor => {
+ // Don't compile less in production!
+ // preview.pro.ant.design only do not use in your production;
+ // preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
+ if (ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION !== 'site') {
+ return;
+ }
+ // Determine if the component is remounted
+ if (!primaryColor) {
+ return;
+ }
+ const hideMessage = message.loading('正在编译主题!', 0);
+ function buildIt() {
+ if (!(window as any).less) {
+ // tslint:disable-next-line no-console
+ return console.log('no less');
+ }
+ setTimeout(() => {
+ (window as any).less
+ .modifyVars({
+ '@primary-color': primaryColor,
+ })
+ .then(() => {
+ hideMessage();
+ })
+ .catch(() => {
+ message.error('Failed to update theme');
+ hideMessage();
+ });
+ }, 200);
+ }
+ if (!lessNodesAppended) {
+ // insert less.js and color.less
+ const lessStyleNode = document.createElement('link');
+ const lessConfigNode = document.createElement('script');
+ const lessScriptNode = document.createElement('script');
+ lessStyleNode.setAttribute('rel', 'stylesheet/less');
+ lessStyleNode.setAttribute('href', '/color.less');
+ lessConfigNode.innerHTML = `
+ window.less = {
+ async: true,
+ env: 'production',
+ javascriptEnabled: true
+ };
+ `;
+ lessScriptNode.src = 'https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js';
+ lessScriptNode.async = true;
+ lessScriptNode.onload = () => {
+ buildIt();
+ lessScriptNode.onload = null;
+ };
+ document.body.appendChild(lessStyleNode);
+ document.body.appendChild(lessConfigNode);
+ document.body.appendChild(lessScriptNode);
+ lessNodesAppended = true;
+ } else {
+ buildIt();
+ }
+};
+
+const updateColorWeak: (colorWeak: boolean) => void = colorWeak => {
+ const root = document.getElementById('root');
+ if (root) {
+ root.className = colorWeak ? 'colorWeak' : '';
+ }
+};
+
+const SettingModel: SettingModelType = {
+ namespace: 'settings',
+ state: defaultSettings,
+ reducers: {
+ getSetting(state = defaultSettings) {
+ const setting: Partial = {};
+ const urlParams = new URL(window.location.href);
+ Object.keys(state).forEach(key => {
+ if (urlParams.searchParams.has(key)) {
+ const value = urlParams.searchParams.get(key);
+ setting[key] = value === '1' ? true : value;
+ }
+ });
+ const { primaryColor, colorWeak } = setting;
+
+ if (state.primaryColor !== primaryColor) {
+ updateTheme(primaryColor);
+ }
+ updateColorWeak(!!colorWeak);
+ return {
+ ...state,
+ ...setting,
+ };
+ },
+ changeSetting(state = defaultSettings, { payload }) {
+ const urlParams = new URL(window.location.href);
+ Object.keys(defaultSettings).forEach(key => {
+ if (urlParams.searchParams.has(key)) {
+ urlParams.searchParams.delete(key);
+ }
+ });
+ Object.keys(payload).forEach(key => {
+ if (key === 'collapse') {
+ return;
+ }
+ let value = payload[key];
+ if (value === true) {
+ value = 1;
+ }
+ if (defaultSettings[key] !== value) {
+ urlParams.searchParams.set(key, value);
+ }
+ });
+ const { primaryColor, colorWeak, contentWidth } = payload;
+ if (state.primaryColor !== primaryColor) {
+ updateTheme(primaryColor);
+ }
+ if (state.contentWidth !== contentWidth && window.dispatchEvent) {
+ window.dispatchEvent(new Event('resize'));
+ }
+ updateColorWeak(colorWeak);
+ window.history.replaceState(null, 'setting', urlParams.href);
+ return {
+ ...state,
+ ...payload,
+ };
+ },
+ },
+};
+export default SettingModel;
diff --git a/thain-fe/src/models/user.ts b/thain-fe/src/models/user.ts
new file mode 100644
index 00000000..f847c763
--- /dev/null
+++ b/thain-fe/src/models/user.ts
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { query as queryUsers, queryCurrent } from '@/services/user';
+import { Effect } from 'dva';
+import { Reducer } from 'redux';
+import { setAuthority } from '@/utils/authority';
+import { reloadAuthorized } from '@/utils/Authorized';
+import { router } from 'umi';
+
+export interface CurrentUser {
+ userId?: string;
+ name?: string;
+ authority?: string[];
+ // 暂未用到的
+ avatar?: string;
+ title?: string;
+ group?: string;
+ signature?: string;
+ geographic?: any;
+ tags?: {
+ key: string;
+ label: string;
+ }[];
+ unreadCount?: number;
+}
+
+export interface UserModelState {
+ currentUser?: CurrentUser;
+}
+
+export interface UserModelType {
+ namespace: 'user';
+ state: UserModelState;
+ effects: {
+ fetch: Effect;
+ fetchCurrent: Effect;
+ };
+ reducers: {
+ saveCurrentUser: Reducer;
+ changeNotifyCount: Reducer;
+ };
+}
+
+const UserModel: UserModelType = {
+ namespace: 'user',
+
+ state: {
+ currentUser: {},
+ },
+
+ effects: {
+ *fetch(_, { call, put }) {
+ const response = yield call(queryUsers);
+ yield put({
+ type: 'save',
+ payload: response,
+ });
+ },
+ *fetchCurrent(_, { call, put }) {
+ const response: CurrentUser | undefined = yield call(queryCurrent);
+ if (response === undefined) {
+ router.push(`/user/login?redirect=${window.location.href}`);
+ return;
+ }
+ setAuthority(response.authority!);
+ reloadAuthorized();
+ yield put({
+ type: 'saveCurrentUser',
+ payload: response,
+ });
+ },
+ },
+
+ reducers: {
+ saveCurrentUser(state, action) {
+ return {
+ ...state,
+ currentUser: action.payload || {},
+ };
+ },
+ changeNotifyCount(
+ state = {
+ currentUser: {},
+ },
+ action,
+ ) {
+ return {
+ ...state,
+ currentUser: {
+ ...state.currentUser,
+ notifyCount: action.payload.totalCount,
+ unreadCount: action.payload.unreadCount,
+ },
+ };
+ },
+ },
+};
+
+export default UserModel;
diff --git a/thain-fe/src/pages/Authorized.tsx b/thain-fe/src/pages/Authorized.tsx
new file mode 100644
index 00000000..b3009276
--- /dev/null
+++ b/thain-fe/src/pages/Authorized.tsx
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import Authorized from '@/utils/Authorized';
+import { ConnectProps, ConnectState, UserModelState, Route } from '@/models/connect';
+import { connect } from 'dva';
+import pathToRegexp from 'path-to-regexp';
+import React from 'react';
+import Redirect from 'umi/redirect';
+
+interface AuthComponentProps extends ConnectProps {
+ user: UserModelState;
+}
+
+const getRouteAuthority = (path: string, routeData: Route[]) => {
+ let authorities: string[] | string | undefined = undefined;
+ routeData.forEach(route => {
+ // match prefix
+ if (pathToRegexp(`${route.path}(.*)`).test(path)) {
+ authorities = route.authority || authorities;
+ // get children authority recursively
+ if (route.routes) {
+ authorities = getRouteAuthority(path, route.routes) || authorities;
+ }
+ }
+ });
+ return authorities;
+};
+
+const AuthComponent: React.FC = ({
+ children,
+ route = {
+ routes: [],
+ },
+ location,
+ user,
+}) => {
+ const { currentUser } = user;
+ const { routes = [] } = route;
+ const isLogin = currentUser && currentUser.name;
+ return (
+ : }
+ >
+ {children}
+
+ );
+};
+
+export default connect(({ user }: ConnectState) => ({
+ user,
+}))(AuthComponent);
diff --git a/thain-fe/src/pages/Dashboard/FlowExecutionStatusCountChart.tsx b/thain-fe/src/pages/Dashboard/FlowExecutionStatusCountChart.tsx
new file mode 100644
index 00000000..bb6c4cdc
--- /dev/null
+++ b/thain-fe/src/pages/Dashboard/FlowExecutionStatusCountChart.tsx
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import ConnectState, { ConnectProps } from '@/models/connect';
+import { Guide, Chart, Geom, Coord, Axis, Legend, Tooltip } from 'bizcharts';
+import DataSet from '@antv/data-set';
+import React from 'react';
+import { connect } from 'dva';
+import { FlowExecutionStatus, getScheduleStatusDesc } from '@/enums/FlowExecutionStatus';
+import LoadingWrapper from './LoadingWrapper';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+interface Props extends ConnectProps {
+ flowExecutionStatusCountLoading: boolean;
+ flowExecutionStatusCount: {
+ status: FlowExecutionStatus;
+ count: number;
+ }[];
+}
+const FlowExecutionStatusCountChart: React.FC = ({
+ flowExecutionStatusCount,
+ flowExecutionStatusCountLoading,
+}) => {
+ if (flowExecutionStatusCountLoading) {
+ return LoadingWrapper(
+ flowExecutionStatusCountLoading,
+ formatMessage({ id: 'flow.execution.status.chart.title' }),
+ ,
+ );
+ }
+ const { DataView } = DataSet;
+ const { Html } = Guide;
+ const total = flowExecutionStatusCount.reduce(
+ (pre, cur) => ({ status: FlowExecutionStatus.SUCCESS, count: pre.count + cur.count }),
+ { status: FlowExecutionStatus.SUCCESS, count: 0 },
+ ).count;
+ const dv = new DataView();
+ dv.source(flowExecutionStatusCount)
+ .transform({
+ type: 'map',
+ callback(row: { status: FlowExecutionStatus; count: number }) {
+ return { status: getScheduleStatusDesc(row.status), count: row.count };
+ },
+ })
+ .transform({
+ type: 'percent',
+ field: 'count',
+ dimension: 'status',
+ as: 'percent',
+ });
+ const cols = {
+ percent: {
+ formatter: (val: number) => {
+ return (val * 100).toFixed(2) + '%';
+ },
+ },
+ };
+
+ return LoadingWrapper(
+ flowExecutionStatusCountLoading,
+ formatMessage({ id: 'flow.execution.status.chart.title' }),
+
+
+
+
+
+
+ ${formatMessage(
+ { id: 'common.total' },
+ )}
${total}`}
+ alignX="middle"
+ alignY="middle"
+ />
+
+ {
+ percent = (percent * 100).toFixed(2) + '%';
+ return {
+ name: status,
+ value: percent,
+ };
+ },
+ ]}
+ style={{
+ stroke: '#fff',
+ }}
+ />
+ ,
+ );
+};
+
+export default connect(({ dashboard }: ConnectState) => ({
+ flowExecutionStatusCountLoading: dashboard.flowExecutionStatusCountLoading,
+ flowExecutionStatusCount: dashboard.flowExecutionStatusCount,
+}))(FlowExecutionStatusCountChart);
diff --git a/thain-fe/src/pages/Dashboard/FlowSourceCountChart.tsx b/thain-fe/src/pages/Dashboard/FlowSourceCountChart.tsx
new file mode 100644
index 00000000..82f51d5e
--- /dev/null
+++ b/thain-fe/src/pages/Dashboard/FlowSourceCountChart.tsx
@@ -0,0 +1,153 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import ConnectState, { ConnectProps } from '@/models/connect';
+import { Axis, Chart, Coord, Geom, Guide, Legend, Tooltip } from 'bizcharts';
+import DataSet from '@antv/data-set';
+import React from 'react';
+import { connect } from 'dva';
+import LoadingWrapper from './LoadingWrapper';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+interface Props extends ConnectProps {
+ flowSourceCountLoading: boolean;
+ flowSourceCount: {
+ source: string;
+ count: number;
+ }[];
+ filterSource: string[];
+}
+const FlowSourceCountChart: React.FC = ({
+ flowSourceCount,
+ filterSource,
+ flowSourceCountLoading,
+ dispatch,
+}) => {
+ if (flowSourceCountLoading) {
+ return LoadingWrapper(
+ flowSourceCountLoading,
+ formatMessage({ id: 'flow.source.chart.title' }),
+ ,
+ );
+ }
+ const { DataView } = DataSet;
+ const { Html } = Guide;
+ const total = flowSourceCount.reduce(
+ (pre, cur) => ({ source: '', count: pre.count + cur.count }),
+ { source: '', count: 0 },
+ ).count;
+ const dv = new DataView();
+
+ dv.source(flowSourceCount).transform({
+ type: 'percent',
+ field: 'count',
+ dimension: 'source',
+ as: 'percent',
+ });
+ const cols = {
+ percent: {
+ formatter: (val: number) => {
+ return (val * 100).toFixed(2) + '%';
+ },
+ },
+ };
+ const changeFilter = (filterArr: string[], val: string, checked: boolean) => {
+ if (!checked) {
+ if (!filterArr.includes(val)) return [...filterArr, val];
+ return filterArr;
+ }
+ return filterArr.filter(origin => origin !== val);
+ };
+ return LoadingWrapper(
+ flowSourceCountLoading,
+ formatMessage({ id: 'flow.source.chart.title' }),
+ {
+ return !filterSource.includes(val);
+ },
+ ],
+ ]}
+ >
+
+
+ ,
+ );
+};
+
+export default connect(({ dashboard }: ConnectState) => ({
+ flowSourceCountLoading: dashboard.flowSourceCountLoading,
+ flowSourceCount: dashboard.flowSourceCount,
+ filterSource: dashboard.filterSource,
+}))(FlowSourceCountChart);
diff --git a/thain-fe/src/pages/Dashboard/IncreaseFlowCountChart.tsx b/thain-fe/src/pages/Dashboard/IncreaseFlowCountChart.tsx
new file mode 100644
index 00000000..956e85c0
--- /dev/null
+++ b/thain-fe/src/pages/Dashboard/IncreaseFlowCountChart.tsx
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import ConnectState, { ConnectProps } from '@/models/connect';
+import React from 'react';
+import { connect } from 'dva';
+import { Statistic } from 'antd';
+import LoadingWrapper from './LoadingWrapper';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+interface Props extends ConnectProps {
+ increaseFlowCountLoading: boolean;
+ increaseFlowCount: number;
+}
+const IncreaseFlowCountChart: React.FC = ({
+ increaseFlowCount,
+ increaseFlowCountLoading,
+}) => {
+ return LoadingWrapper(
+ increaseFlowCountLoading,
+ formatMessage({ id: 'flow.increase.chart.title' }),
+ ,
+ );
+};
+
+export default connect(({ dashboard }: ConnectState) => ({
+ increaseFlowCountLoading: dashboard.increaseFlowCountLoading,
+ increaseFlowCount: dashboard.increaseFlowCount,
+}))(IncreaseFlowCountChart);
diff --git a/thain-fe/src/pages/Dashboard/IncreaseJobCountChart.tsx b/thain-fe/src/pages/Dashboard/IncreaseJobCountChart.tsx
new file mode 100644
index 00000000..c68f7b89
--- /dev/null
+++ b/thain-fe/src/pages/Dashboard/IncreaseJobCountChart.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import ConnectState, { ConnectProps } from '@/models/connect';
+import React from 'react';
+import { connect } from 'dva';
+import { Statistic } from 'antd';
+import LoadingWrapper from './LoadingWrapper';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+interface Props extends ConnectProps {
+ increaseJobCountLoading: boolean;
+ increaseJobCount: number;
+}
+const IncreaseJobCountChart: React.FC = ({ increaseJobCount, increaseJobCountLoading }) => {
+ return LoadingWrapper(
+ increaseJobCountLoading,
+ formatMessage({ id: 'job.increase.chart.title' }),
+ ,
+ );
+};
+
+export default connect(({ dashboard }: ConnectState) => ({
+ increaseJobCountLoading: dashboard.increaseJobCountLoading,
+ increaseJobCount: dashboard.increaseJobCount,
+}))(IncreaseJobCountChart);
diff --git a/thain-fe/src/pages/Dashboard/JobExecutionStatusCountChart.tsx b/thain-fe/src/pages/Dashboard/JobExecutionStatusCountChart.tsx
new file mode 100644
index 00000000..d5dd2a53
--- /dev/null
+++ b/thain-fe/src/pages/Dashboard/JobExecutionStatusCountChart.tsx
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import ConnectState, { ConnectProps } from '@/models/connect';
+import { Guide, Chart, Geom, Coord, Axis, Legend, Tooltip } from 'bizcharts';
+import DataSet from '@antv/data-set';
+import React from 'react';
+import { connect } from 'dva';
+import { JobExecutionStatus, getScheduleStatusDesc } from '@/enums/JobExecutionStatus';
+import LoadingWrapper from './LoadingWrapper';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+interface Props extends ConnectProps {
+ jobExecutionStatusCountLoading: boolean;
+ jobExecutionStatusCount: {
+ status: JobExecutionStatus;
+ count: number;
+ }[];
+}
+const JobExecutionStatusCountChart: React.FC = ({
+ jobExecutionStatusCount,
+ jobExecutionStatusCountLoading,
+}) => {
+ if (jobExecutionStatusCountLoading) {
+ return LoadingWrapper(
+ jobExecutionStatusCountLoading,
+ formatMessage({ id: 'job.execution.status.chart.title' }),
+ ,
+ );
+ }
+ const { DataView } = DataSet;
+ const { Html } = Guide;
+ const total = jobExecutionStatusCount.reduce(
+ (pre, cur) => ({ status: JobExecutionStatus.SUCCESS, count: pre.count + cur.count }),
+ { status: JobExecutionStatus.SUCCESS, count: 0 },
+ ).count;
+ const dv = new DataView();
+ dv.source(jobExecutionStatusCount)
+ .transform({
+ type: 'map',
+ callback(row: { status: JobExecutionStatus; count: number }) {
+ return { status: getScheduleStatusDesc(row.status), count: row.count };
+ },
+ })
+ .transform({
+ type: 'percent',
+ field: 'count',
+ dimension: 'status',
+ as: 'percent',
+ });
+ const cols = {
+ percent: {
+ formatter: (val: number) => {
+ return (val * 100).toFixed(2) + '%';
+ },
+ },
+ };
+
+ return LoadingWrapper(
+ jobExecutionStatusCountLoading,
+ formatMessage({ id: 'job.execution.status.chart.title' }),
+
+
+
+
+
+
+ ${formatMessage(
+ { id: 'common.total' },
+ )}
${total}`}
+ alignX="middle"
+ alignY="middle"
+ />
+
+ {
+ percent = (percent * 100).toFixed(2) + '%';
+ return {
+ name: status,
+ value: percent,
+ };
+ },
+ ]}
+ style={{
+ stroke: '#fff',
+ }}
+ />
+ ,
+ );
+};
+
+export default connect(({ dashboard }: ConnectState) => ({
+ jobExecutionStatusCountLoading: dashboard.jobExecutionStatusCountLoading,
+ jobExecutionStatusCount: dashboard.jobExecutionStatusCount,
+}))(JobExecutionStatusCountChart);
diff --git a/thain-fe/src/pages/Dashboard/JobExecutionStatusHistoryCountChart.tsx b/thain-fe/src/pages/Dashboard/JobExecutionStatusHistoryCountChart.tsx
new file mode 100644
index 00000000..dc84860f
--- /dev/null
+++ b/thain-fe/src/pages/Dashboard/JobExecutionStatusHistoryCountChart.tsx
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+import { Chart, Geom, Axis, Tooltip, Legend } from 'bizcharts';
+import ConnectState, { ConnectProps } from '@/models/connect';
+import { connect } from 'dva';
+import LoadingWrapper from './LoadingWrapper';
+import moment from 'moment';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+interface Props extends ConnectProps {
+ statusHistoryCountLoading: boolean;
+ statusHistoryCount: {
+ status: number;
+ time: string;
+ count: number;
+ }[];
+}
+
+const JobExecutionStatusHistoryCountChart: React.FC = ({
+ statusHistoryCount,
+ statusHistoryCountLoading,
+}) => {
+ if (statusHistoryCountLoading) {
+ return LoadingWrapper(
+ statusHistoryCountLoading,
+ formatMessage({ id: 'job.execution.history.chart.title' }),
+ ,
+ );
+ }
+ const cols = {
+ month: {
+ range: [0, 1],
+ },
+ };
+
+ const formatData = statusHistoryCount.map(countData => {
+ const longTime = countData.time;
+ const time = longTime
+ .split('~')
+ .map(t => moment.unix(Number(t)).format('YYYY-MM-DD HH:mm:ss'))
+ .reduce((pre, cur) => {
+ return pre + '\n ~ \n' + cur;
+ });
+ return {
+ ...countData,
+ time,
+ status:
+ countData.status === 2
+ ? formatMessage({ id: 'common.success' })
+ : formatMessage({ id: 'common.failed' }),
+ };
+ });
+ return LoadingWrapper(
+ statusHistoryCountLoading,
+ formatMessage({ id: 'job.execution.history.chart.title' }),
+
+
+
+
+
+
+
+ ,
+ );
+};
+
+export default connect(({ dashboard }: ConnectState) => ({
+ statusHistoryCountLoading: dashboard.statusHistoryCountLoading,
+ statusHistoryCount: dashboard.statusHistoryCount,
+}))(JobExecutionStatusHistoryCountChart);
diff --git a/thain-fe/src/pages/Dashboard/LoadingWrapper.tsx b/thain-fe/src/pages/Dashboard/LoadingWrapper.tsx
new file mode 100644
index 00000000..d8e2f632
--- /dev/null
+++ b/thain-fe/src/pages/Dashboard/LoadingWrapper.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+import { Card, Spin } from 'antd';
+
+const LoadingWrapper = (loading: boolean, title: string, innerChart: JSX.Element) => (
+
+ {loading ? (
+
+ ) : (
+ innerChart
+ )}
+
+);
+export default LoadingWrapper;
diff --git a/thain-fe/src/pages/Dashboard/RunningFlowCountChart.tsx b/thain-fe/src/pages/Dashboard/RunningFlowCountChart.tsx
new file mode 100644
index 00000000..18f138e9
--- /dev/null
+++ b/thain-fe/src/pages/Dashboard/RunningFlowCountChart.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import ConnectState, { ConnectProps } from '@/models/connect';
+import React from 'react';
+import { connect } from 'dva';
+import { Statistic } from 'antd';
+import LoadingWrapper from './LoadingWrapper';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+interface Props extends ConnectProps {
+ runningFlowCountLoading: boolean;
+ runningFlowCount: number;
+}
+const RunningFlowCountChart: React.FC = ({ runningFlowCount, runningFlowCountLoading }) => {
+ return LoadingWrapper(
+ runningFlowCountLoading,
+ formatMessage({ id: 'flow.running.chart.title' }),
+ ,
+ );
+};
+
+export default connect(({ dashboard }: ConnectState) => ({
+ runningFlowCountLoading: dashboard.runningFlowCountLoading,
+ runningFlowCount: dashboard.runningFlowCount,
+}))(RunningFlowCountChart);
diff --git a/thain-fe/src/pages/Dashboard/RunningJobCountChart.tsx b/thain-fe/src/pages/Dashboard/RunningJobCountChart.tsx
new file mode 100644
index 00000000..825fa6d6
--- /dev/null
+++ b/thain-fe/src/pages/Dashboard/RunningJobCountChart.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import ConnectState, { ConnectProps } from '@/models/connect';
+import React from 'react';
+import { connect } from 'dva';
+import { Statistic } from 'antd';
+import LoadingWrapper from './LoadingWrapper';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+interface Props extends ConnectProps {
+ runningJobCountLoading: boolean;
+ runningJobCount: number;
+}
+const RunningJobCountChart: React.FC = ({ runningJobCount, runningJobCountLoading }) => {
+ return LoadingWrapper(
+ runningJobCountLoading,
+ formatMessage({ id: 'job.running.chart.title' }),
+ ,
+ );
+};
+
+export default connect(({ dashboard }: ConnectState) => ({
+ runningJobCountLoading: dashboard.runningJobCountLoading,
+ runningJobCount: dashboard.runningJobCount,
+}))(RunningJobCountChart);
diff --git a/thain-fe/src/pages/Dashboard/ScheduleStatusCountChart.tsx b/thain-fe/src/pages/Dashboard/ScheduleStatusCountChart.tsx
new file mode 100644
index 00000000..946ce1cf
--- /dev/null
+++ b/thain-fe/src/pages/Dashboard/ScheduleStatusCountChart.tsx
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import ConnectState, { ConnectProps } from '@/models/connect';
+import { Guide, Chart, Geom, Coord, Axis, Legend, Tooltip } from 'bizcharts';
+import DataSet from '@antv/data-set';
+import React from 'react';
+import { connect } from 'dva';
+import {
+ FlowSchedulingStatus,
+ getScheduleStatusDesc,
+ getScheduleStatusCode,
+} from '@/enums/FlowSchedulingStatus';
+import LoadingWrapper from './LoadingWrapper';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+interface Props extends ConnectProps {
+ scheduleStatusLoading: boolean;
+ scheduleStatusCount: {
+ status: FlowSchedulingStatus;
+ count: number;
+ }[];
+ filterScheduleStatus: number[];
+}
+const ScheduleStatusCountChart: React.FC = ({
+ scheduleStatusLoading,
+ scheduleStatusCount,
+ filterScheduleStatus,
+ dispatch,
+}) => {
+ if (scheduleStatusLoading) {
+ return LoadingWrapper(
+ scheduleStatusLoading,
+ formatMessage({ id: 'flow.schedule.status.chart.title' }),
+ ,
+ );
+ }
+ const { DataView } = DataSet;
+ const { Html } = Guide;
+ const dv = new DataView();
+ const total = scheduleStatusCount.reduce(
+ (pre, cur) => ({ status: FlowSchedulingStatus.NOT_SET, count: pre.count + cur.count }),
+ { status: FlowSchedulingStatus.NOT_SET, count: 0 },
+ ).count;
+ dv.source(scheduleStatusCount)
+ .transform({
+ type: 'map',
+ callback(row: { status: FlowSchedulingStatus; count: number }) {
+ return { status: getScheduleStatusDesc(row.status), count: row.count };
+ },
+ })
+ .transform({
+ type: 'percent',
+ field: 'count',
+ dimension: 'status',
+ as: 'percent',
+ });
+ const cols = {
+ percent: {
+ formatter: (val: number) => {
+ return (val * 100).toFixed(2) + '%';
+ },
+ },
+ };
+
+ const changeFilter = (filterArr: number[], val: number, checked: boolean) => {
+ if (!checked) {
+ if (!filterArr.includes(val)) return [...filterArr, val];
+ return filterArr;
+ }
+ return filterArr.filter(origin => origin !== val);
+ };
+ return LoadingWrapper(
+ scheduleStatusLoading,
+ formatMessage({ id: 'flow.schedule.status.chart.title' }),
+ {
+ return !filterScheduleStatus.includes(getScheduleStatusCode(val));
+ },
+ ],
+ ]}
+ >
+
+
+ ,
+ );
+};
+
+export default connect(({ dashboard }: ConnectState) => ({
+ scheduleStatusLoading: dashboard.scheduleStatusLoading,
+ scheduleStatusCount: dashboard.scheduleStatusCount,
+ filterScheduleStatus: dashboard.filterScheduleStatus,
+}))(ScheduleStatusCountChart);
diff --git a/thain-fe/src/pages/Dashboard/index.tsx b/thain-fe/src/pages/Dashboard/index.tsx
new file mode 100644
index 00000000..0432f301
--- /dev/null
+++ b/thain-fe/src/pages/Dashboard/index.tsx
@@ -0,0 +1,248 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import * as React from 'react';
+import { useEffect } from 'react';
+import { Col, DatePicker, Row, Select } from 'antd';
+import ScheduleStatusCountChart from './ScheduleStatusCountChart';
+import JobExecutionStatusHistoryCountChart from './JobExecutionStatusHistoryCountChart';
+import RunningFlowCountChart from './RunningFlowCountChart';
+import IncreaseJobCountChart from './IncreaseJobCountChart';
+import IncreaseFlowCountChart from './IncreaseFlowCountChart';
+import RunningJobCountChart from './RunningJobCountChart';
+import JobExecutionStatusCountChart from './JobExecutionStatusCountChart';
+import FlowSourceCountChart from './FlowSourceCountChart';
+import FlowExecutionStatusCountChart from './FlowExecutionStatusCountChart';
+import { connect } from 'dva';
+import ConnectState, { ConnectProps } from '@/models/connect';
+import { FlowSchedulingStatus } from '@/enums/FlowSchedulingStatus';
+import { RangePickerPresetRange } from 'antd/lib/date-picker/interface';
+import moment from 'moment';
+import { formatMessage } from 'umi-plugin-react/locale';
+const { RangePicker } = DatePicker;
+
+const colLayout = {
+ xs: 24,
+ sm: 24,
+ md: 12,
+ lg: 12,
+ xl: 12,
+ xxl: 6,
+};
+interface Props extends ConnectProps {
+ firstHistoryPeriod: number[];
+ secondHistoryPeriod: number[];
+ filterScheduleStatus: FlowSchedulingStatus[];
+ filterSource: string[];
+ maxPointNum: number;
+}
+const Analysis: React.FC = ({
+ dispatch,
+ firstHistoryPeriod,
+ secondHistoryPeriod,
+ filterScheduleStatus,
+ filterSource,
+ maxPointNum,
+}) => {
+ useEffect(() => {
+ if (dispatch) {
+ dispatch({
+ type: 'dashboard/fetchScheduleStatusCount',
+ });
+ }
+ }, [filterSource]);
+
+ useEffect(() => {
+ if (dispatch) {
+ dispatch({
+ type: 'dashboard/fetchFlowSourceCount',
+ });
+ }
+ }, [filterScheduleStatus]);
+
+ useEffect(() => {
+ if (dispatch) {
+ dispatch({
+ type: 'dashboard/fetchRunningFlowCount',
+ });
+ dispatch({
+ type: 'dashboard/fetchRunningJobCount',
+ });
+ }
+ }, [filterScheduleStatus, filterSource]);
+
+ useEffect(() => {
+ if (dispatch) {
+ dispatch({
+ type: 'dashboard/fetchFlowExecutionStatusCount',
+ });
+ dispatch({
+ type: 'dashboard/fetchJobExecutionStatusCount',
+ });
+ dispatch({
+ type: 'dashboard/fetchIncreaseFlowCount',
+ });
+ dispatch({
+ type: 'dashboard/fetchIncreaseJobCount',
+ });
+ }
+ }, [firstHistoryPeriod]);
+
+ useEffect(() => {
+ if (dispatch) {
+ dispatch({
+ type: 'dashboard/fetchStatusHistoryCount',
+ });
+ }
+ }, [secondHistoryPeriod, maxPointNum]);
+
+ const firstRangeChange = (dates: RangePickerPresetRange) => {
+ if (dispatch) {
+ dispatch({
+ type: 'dashboard/updateState',
+ payload: { firstHistoryPeriod: [dates[0] && dates[0].unix(), dates[1] && dates[1].unix()] },
+ });
+ }
+ };
+
+ const secondRangeChange = (dates: RangePickerPresetRange) => {
+ if (dispatch) {
+ dispatch({
+ type: 'dashboard/updateState',
+ payload: {
+ secondHistoryPeriod: [dates[0] && dates[0].unix(), dates[1] && dates[1].unix()],
+ },
+ });
+ }
+ };
+ const maxPointNumChange = (value: number) => {
+ if (dispatch) {
+ dispatch({
+ type: 'dashboard/updateState',
+ payload: {
+ maxPointNum: value,
+ },
+ });
+ }
+ };
+ const { Option } = Select;
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatMessage({ id: 'common.statistical.period' })}:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatMessage({ id: 'common.statistical.period' })}:
+
+
+
+ {formatMessage({ id: 'common.polygon.max.num' })}:
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default connect(({ dashboard }: ConnectState) => ({
+ firstHistoryPeriod: dashboard.firstHistoryPeriod,
+ secondHistoryPeriod: dashboard.secondHistoryPeriod,
+ filterScheduleStatus: dashboard.filterScheduleStatus,
+ filterSource: dashboard.filterSource,
+ maxPointNum: dashboard.maxPointNum,
+}))(Analysis);
diff --git a/thain-fe/src/pages/Dashboard/model.ts b/thain-fe/src/pages/Dashboard/model.ts
new file mode 100644
index 00000000..508943d8
--- /dev/null
+++ b/thain-fe/src/pages/Dashboard/model.ts
@@ -0,0 +1,276 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+/**
+ * dashboard的model
+ */
+
+import { Effect } from 'dva';
+import { Reducer } from 'redux';
+import { FlowSchedulingStatus } from '@/enums/FlowSchedulingStatus';
+import { FlowExecutionStatus } from '@/enums/FlowExecutionStatus';
+import { JobExecutionStatus } from '@/enums/JobExecutionStatus';
+import ConnectState from '@/models/connect';
+import {
+ getScheduleStatusCount,
+ getFlowSourceCount,
+ getFlowExecutionStatusCount,
+ getRunningFlowCount,
+ getIncreaseFlowCount,
+ getIncreaseJobCount,
+ getRunningJobCount,
+ getStatusHistoryCount,
+ getJobExecutionStatusCount,
+} from './service';
+import moment from 'moment';
+
+export class DashboardState {
+ scheduleStatusCount?: {
+ status: FlowSchedulingStatus;
+ count: number;
+ }[];
+ flowSourceCount?: {
+ source: string;
+ count: number;
+ }[];
+ flowExecutionStatusCount?: {
+ status: FlowExecutionStatus;
+ count: number;
+ }[];
+ jobExecutionStatusCount?: {
+ status: JobExecutionStatus;
+ count: number;
+ }[];
+ runningFlowCount?: number;
+ runningJobCount?: number;
+ increaseFlowCount?: number;
+ increaseJobCount?: number;
+ statusHistoryCount?: {
+ status: number;
+ time: string;
+ count: number;
+ }[];
+ scheduleStatusLoading = true;
+ flowSourceCountLoading = true;
+ flowExecutionStatusCountLoading = true;
+ jobExecutionStatusCountLoading = true;
+ runningJobCountLoading = true;
+ runningFlowCountLoading = true;
+ increaseFlowCountLoading = true;
+ increaseJobCountLoading = true;
+ statusHistoryCountLoading = true;
+ firstHistoryPeriod = [
+ moment()
+ .add({ day: -1 })
+ .unix(),
+ moment().unix(),
+ ];
+ secondHistoryPeriod = [
+ moment()
+ .add({ day: -1 })
+ .unix(),
+ moment().unix(),
+ ];
+ maxPointNum: number = 10;
+ filterScheduleStatus = [];
+ filterSource = [];
+}
+
+interface DashboardModelType {
+ namespace: 'dashboard';
+ state: DashboardState;
+ effects: {
+ fetchScheduleStatusCount: Effect;
+ fetchFlowSourceCount: Effect;
+ fetchFlowExecutionStatusCount: Effect;
+ fetchJobExecutionStatusCount: Effect;
+ fetchRunningJobCount: Effect;
+ fetchRunningFlowCount: Effect;
+ fetchIncreaseFlowCount: Effect;
+ fetchIncreaseJobCount: Effect;
+ fetchStatusHistoryCount: Effect;
+ };
+ reducers: {
+ updateState: Reducer;
+ unmount: Reducer;
+ };
+}
+
+function* loadingWrapper(loadingAttr: string, put: any) {
+ yield put({
+ type: 'updateState',
+ payload: { [loadingAttr]: true },
+ });
+ yield put({
+ type: 'updateState',
+ payload: { [loadingAttr]: false },
+ });
+}
+
+const DashboardModel: DashboardModelType = {
+ namespace: 'dashboard',
+ state: new DashboardState(),
+
+ effects: {
+ *fetchScheduleStatusCount(_, { call, put, select }) {
+ const state: DashboardState = yield select((s: ConnectState) => s.dashboard);
+ const g = loadingWrapper('scheduleStatusLoading', put);
+ yield g.next().value;
+ const scheduleStatusCount: {
+ status: FlowSchedulingStatus;
+ count: number;
+ }[] = yield call(getScheduleStatusCount, state.filterSource);
+ if (scheduleStatusCount !== undefined) {
+ yield put({
+ type: 'updateState',
+ payload: { scheduleStatusCount },
+ });
+ yield g.next().value;
+ }
+ },
+ *fetchFlowSourceCount(_, { call, put, select }) {
+ const state: DashboardState = yield select((s: ConnectState) => s.dashboard);
+ const g = loadingWrapper('flowSourceCountLoading', put);
+ yield g.next().value;
+ const flowSourceCount: {
+ source: string;
+ count: number;
+ }[] = yield call(getFlowSourceCount, state.filterScheduleStatus);
+ if (flowSourceCount !== undefined) {
+ yield put({
+ type: 'updateState',
+ payload: { flowSourceCount },
+ });
+ yield g.next().value;
+ }
+ },
+ *fetchFlowExecutionStatusCount(_, { call, put, select }) {
+ const state: DashboardState = yield select((s: ConnectState) => s.dashboard);
+ const g = loadingWrapper('flowExecutionStatusCountLoading', put);
+ yield g.next().value;
+ const flowExecutionStatusCount: {
+ status: FlowExecutionStatus;
+ count: number;
+ }[] = yield call(getFlowExecutionStatusCount, state.firstHistoryPeriod);
+ if (flowExecutionStatusCount !== undefined) {
+ yield put({
+ type: 'updateState',
+ payload: { flowExecutionStatusCount },
+ });
+ yield g.next().value;
+ }
+ },
+ *fetchJobExecutionStatusCount(_, { call, put, select }) {
+ const state: DashboardState = yield select((s: ConnectState) => s.dashboard);
+ const g = loadingWrapper('jobExecutionStatusCountLoading', put);
+ yield g.next().value;
+ const jobExecutionStatusCount: {
+ status: JobExecutionStatus;
+ count: number;
+ }[] = yield call(getJobExecutionStatusCount, state.firstHistoryPeriod);
+ if (jobExecutionStatusCount !== undefined) {
+ yield put({
+ type: 'updateState',
+ payload: { jobExecutionStatusCount },
+ });
+ yield g.next().value;
+ }
+ },
+
+ *fetchRunningFlowCount(_, { call, put, select }) {
+ const state: DashboardState = yield select((s: ConnectState) => s.dashboard);
+ const g = loadingWrapper('runningFlowCountLoading', put);
+ yield g.next().value;
+ const runningFlowCount: number = yield call(
+ getRunningFlowCount,
+ state.filterSource,
+ state.filterScheduleStatus,
+ );
+ if (runningFlowCount !== undefined) {
+ yield put({
+ type: 'updateState',
+ payload: { runningFlowCount },
+ });
+ yield g.next().value;
+ }
+ },
+ *fetchRunningJobCount(_, { call, put, select }) {
+ const state: DashboardState = yield select((s: ConnectState) => s.dashboard);
+ const g = loadingWrapper('runningJobCountLoading', put);
+ yield g.next().value;
+ const runningJobCount: number = yield call(
+ getRunningJobCount,
+ state.filterSource,
+ state.filterScheduleStatus,
+ );
+ if (runningJobCount !== undefined) {
+ yield put({
+ type: 'updateState',
+ payload: { runningJobCount },
+ });
+ yield g.next().value;
+ }
+ },
+
+ *fetchIncreaseFlowCount(_, { call, put, select }) {
+ const state: DashboardState = yield select((s: ConnectState) => s.dashboard);
+ const g = loadingWrapper('increaseFlowCountLoading', put);
+ yield g.next().value;
+ const increaseFlowCount: number = yield call(getIncreaseFlowCount, state.firstHistoryPeriod);
+ if (increaseFlowCount !== undefined) {
+ yield put({
+ type: 'updateState',
+ payload: { increaseFlowCount },
+ });
+ yield g.next().value;
+ }
+ },
+
+ *fetchIncreaseJobCount(_, { call, put, select }) {
+ const state: DashboardState = yield select((s: ConnectState) => s.dashboard);
+ const g = loadingWrapper('increaseJobCountLoading', put);
+ yield g.next().value;
+ const increaseJobCount: number = yield call(getIncreaseJobCount, state.firstHistoryPeriod);
+ if (increaseJobCount !== undefined) {
+ yield put({
+ type: 'updateState',
+ payload: { increaseJobCount },
+ });
+ yield g.next().value;
+ }
+ },
+
+ *fetchStatusHistoryCount(_, { call, put, select }) {
+ const state: DashboardState = yield select((s: ConnectState) => s.dashboard);
+ const g = loadingWrapper('statusHistoryCountLoading', put);
+ yield g.next().value;
+ const statusHistoryCount: {
+ status: string;
+ time: string;
+ count: string;
+ }[] = yield call(getStatusHistoryCount, state.secondHistoryPeriod, state.maxPointNum);
+ if (statusHistoryCount !== undefined) {
+ yield put({
+ type: 'updateState',
+ payload: { statusHistoryCount },
+ });
+ yield g.next().value;
+ }
+ },
+ },
+ reducers: {
+ updateState(state, action) {
+ return {
+ ...state,
+ ...action.payload,
+ };
+ },
+ unmount() {
+ return new DashboardState();
+ },
+ },
+};
+
+export default DashboardModel;
diff --git a/thain-fe/src/pages/Dashboard/service.ts b/thain-fe/src/pages/Dashboard/service.ts
new file mode 100644
index 00000000..3a0b9906
--- /dev/null
+++ b/thain-fe/src/pages/Dashboard/service.ts
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { FlowSchedulingStatus } from '@/enums/FlowSchedulingStatus';
+import { get } from '@/utils/request';
+import { FlowExecutionStatus } from '@/enums/FlowExecutionStatus';
+import { JobExecutionStatus } from '@/enums/JobExecutionStatus';
+
+export async function getScheduleStatusCount(filterSource: string[]) {
+ return get<
+ {
+ status: FlowSchedulingStatus;
+ count: number;
+ }[]
+ >('/api/dashboard/schedule-status-count', { filterSource });
+}
+
+export async function getFlowSourceCount(
+ filterScheduleStatus: [
+ FlowSchedulingStatus.NOT_SET,
+ FlowSchedulingStatus.PAUSE,
+ FlowSchedulingStatus.SCHEDULING,
+ ],
+) {
+ return get<
+ {
+ source: string;
+ count: number;
+ }[]
+ >('/api/dashboard/flow-source-count', { filterScheduleStatus });
+}
+
+export async function getFlowExecutionStatusCount(period: number[]) {
+ return get<
+ {
+ status: FlowExecutionStatus;
+ count: number;
+ }[]
+ >('/api/dashboard/flow-execution-status-count', { period });
+}
+
+export async function getJobExecutionStatusCount(period: number[]) {
+ return get<
+ {
+ status: JobExecutionStatus;
+ count: number;
+ }[]
+ >('/api/dashboard/job-execution-status-count', { period });
+}
+
+export async function getRunningFlowCount(
+ filterSource: string[],
+ filterScheduleStatus: [
+ FlowSchedulingStatus.NOT_SET,
+ FlowSchedulingStatus.PAUSE,
+ FlowSchedulingStatus.SCHEDULING,
+ ],
+) {
+ return get('/api/dashboard/running-flow-count', { filterSource, filterScheduleStatus });
+}
+
+export async function getRunningJobCount(
+ filterSource: string[],
+ filterScheduleStatus: [
+ FlowSchedulingStatus.NOT_SET,
+ FlowSchedulingStatus.PAUSE,
+ FlowSchedulingStatus.SCHEDULING,
+ ],
+) {
+ return get('/api/dashboard/running-job-count', { filterSource, filterScheduleStatus });
+}
+
+export async function getIncreaseFlowCount(period: number[]) {
+ return get('/api/dashboard/increase-flow-count', { period });
+}
+
+export async function getIncreaseJobCount(period: number[]) {
+ return get('/api/dashboard/increase-job-count', { period });
+}
+
+export async function getStatusHistoryCount(period: number[], maxPointNum: number) {
+ return get<
+ {
+ status: string;
+ time: string;
+ count: string;
+ }[]
+ >('/api/dashboard/status-history-count', { period, maxPointNum });
+}
diff --git a/thain-fe/src/pages/Exception/403/index.tsx b/thain-fe/src/pages/Exception/403/index.tsx
new file mode 100644
index 00000000..96a0e223
--- /dev/null
+++ b/thain-fe/src/pages/Exception/403/index.tsx
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+import { formatMessage } from 'umi-plugin-react/locale';
+import Link from 'umi/link';
+import Exception from '@/components/Exception';
+
+export default () => (
+
+);
diff --git a/thain-fe/src/pages/Exception/403/locales/en-US.ts b/thain-fe/src/pages/Exception/403/locales/en-US.ts
new file mode 100644
index 00000000..5007a916
--- /dev/null
+++ b/thain-fe/src/pages/Exception/403/locales/en-US.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ 'exception-403.exception.back': 'Back to home',
+ 'exception-403.description.403': "Sorry, you don't have access to this page",
+};
diff --git a/thain-fe/src/pages/Exception/403/locales/zh-CN.ts b/thain-fe/src/pages/Exception/403/locales/zh-CN.ts
new file mode 100644
index 00000000..8026cb02
--- /dev/null
+++ b/thain-fe/src/pages/Exception/403/locales/zh-CN.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ 'exception-403.exception.back': '返回首页',
+ 'exception-403.description.403': '抱歉,你无权访问该页面',
+};
diff --git a/thain-fe/src/pages/Exception/404/index.tsx b/thain-fe/src/pages/Exception/404/index.tsx
new file mode 100644
index 00000000..f8efe319
--- /dev/null
+++ b/thain-fe/src/pages/Exception/404/index.tsx
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+import { formatMessage } from 'umi-plugin-react/locale';
+import Link from 'umi/link';
+import Exception from '@/components/Exception';
+
+export default () => (
+
+);
diff --git a/thain-fe/src/pages/Exception/404/locales/en-US.ts b/thain-fe/src/pages/Exception/404/locales/en-US.ts
new file mode 100644
index 00000000..f16846fb
--- /dev/null
+++ b/thain-fe/src/pages/Exception/404/locales/en-US.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ 'exception-404.exception.back': 'Back to home',
+ 'exception-404.description.404': 'Sorry, the page you visited does not exist',
+};
diff --git a/thain-fe/src/pages/Exception/404/locales/zh-CN.ts b/thain-fe/src/pages/Exception/404/locales/zh-CN.ts
new file mode 100644
index 00000000..13eedd5d
--- /dev/null
+++ b/thain-fe/src/pages/Exception/404/locales/zh-CN.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ 'exception-404.exception.back': '返回首页',
+ 'exception-404.description.404': '抱歉,你访问的页面不存在',
+};
diff --git a/thain-fe/src/pages/Exception/500/index.tsx b/thain-fe/src/pages/Exception/500/index.tsx
new file mode 100644
index 00000000..12002d8d
--- /dev/null
+++ b/thain-fe/src/pages/Exception/500/index.tsx
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import React from 'react';
+import { formatMessage } from 'umi-plugin-react/locale';
+import Link from 'umi/link';
+import Exception from '@/components/Exception';
+
+export default () => (
+
+);
diff --git a/thain-fe/src/pages/Exception/500/locales/en-US.ts b/thain-fe/src/pages/Exception/500/locales/en-US.ts
new file mode 100644
index 00000000..d7ea5c88
--- /dev/null
+++ b/thain-fe/src/pages/Exception/500/locales/en-US.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ 'exception-500.exception.back': 'Back to home',
+ 'exception-500.description.500': 'Sorry, the server is reporting an error',
+};
diff --git a/thain-fe/src/pages/Exception/500/locales/zh-CN.ts b/thain-fe/src/pages/Exception/500/locales/zh-CN.ts
new file mode 100644
index 00000000..a3353ca9
--- /dev/null
+++ b/thain-fe/src/pages/Exception/500/locales/zh-CN.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export default {
+ 'exception-500.exception.back': '返回首页',
+ 'exception-500.description.500': '抱歉,服务器出错了',
+};
diff --git a/thain-fe/src/pages/Flow/List/FlowTable.tsx b/thain-fe/src/pages/Flow/List/FlowTable.tsx
new file mode 100644
index 00000000..cc5547da
--- /dev/null
+++ b/thain-fe/src/pages/Flow/List/FlowTable.tsx
@@ -0,0 +1,354 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import {
+ Button,
+ Col,
+ Dropdown,
+ Icon,
+ Menu,
+ notification,
+ Popconfirm,
+ Row,
+ Table,
+ Tooltip,
+} from 'antd';
+import ButtonGroup from 'antd/lib/button/button-group';
+import React, { useState } from 'react';
+import { router } from 'umi';
+import { ConnectProps, ConnectState } from '@/models/connect';
+import { connect } from 'dva';
+import { TableResult } from '@/typings/ApiResult';
+import { FlowLastRunStatus } from '@/enums/FlowLastRunStatus';
+import { FlowSchedulingStatus } from '@/enums/FlowSchedulingStatus';
+import { FlowModel } from '@/commonModels/FlowModel';
+import { PaginationConfig, SorterResult } from 'antd/lib/table';
+import { ClickParam } from 'antd/es/menu';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+interface Props extends ConnectProps<{ flowId: number }> {
+ tableResult: TableResult;
+ loading: boolean;
+}
+
+const FlowTable: React.FC = ({
+ tableResult: { data, count, page, pageSize },
+ loading,
+ dispatch,
+}) => {
+ const [batchId, setBatchId] = useState([]);
+ function tableChange(
+ pagination: PaginationConfig,
+ filters: Record,
+ sorter: SorterResult,
+ ) {
+ const sort = sorter.columnKey && {
+ key: sorter.columnKey,
+ orderDesc: sorter.order === 'descend',
+ };
+ if (dispatch) {
+ dispatch({
+ type: 'flowList/fetchTable',
+ payload: {
+ page: pagination.current,
+ pageSize: pagination.pageSize,
+ sort,
+ },
+ });
+ }
+ }
+ function renderButton(flow: FlowModel) {
+ if (flow.schedulingStatus) {
+ switch (flow.schedulingStatus) {
+ case FlowSchedulingStatus.NOT_SET:
+ return (
+
+ );
+ case FlowSchedulingStatus.PAUSE:
+ return (
+
+ );
+ case FlowSchedulingStatus.SCHEDULING:
+ return (
+
+ );
+ default:
+ return ;
+ }
+ }
+ return ;
+ }
+ const columns = [
+ { title: 'ID', dataIndex: 'id', key: 'id', sorter: true, fixed: 'left' },
+ {
+ title: formatMessage({ id: 'flow.name' }),
+ dataIndex: 'name',
+ key: 'name',
+ fixed: 'left',
+ width: 350,
+ render: (name: string) => {
+ return (
+
+
+ {name}
+
+
+ );
+ },
+ },
+ { title: formatMessage({ id: 'flow.cron' }), dataIndex: 'cron', key: 'cron' },
+ {
+ title: formatMessage({ id: 'flow.last.status' }),
+ dataIndex: 'lastRunStatus',
+ key: 'lastRunStatus',
+ render: (status: number) => FlowLastRunStatus[status],
+ },
+ {
+ title: formatMessage({ id: 'flow.schedule.status' }),
+ dataIndex: 'schedulingStatus',
+ key: 'schedulingStatus',
+ render: (status: number) => FlowSchedulingStatus[status],
+ },
+ {
+ title: formatMessage({ id: 'flow.status.update.time' }),
+ dataIndex: 'statusUpdateTime',
+ key: 'status_update_time',
+ sorter: true,
+ render(time: number) {
+ return new Date(time).toLocaleString();
+ },
+ },
+
+ {
+ title: formatMessage({ id: 'flow.create.user' }),
+ dataIndex: 'createUser',
+ key: 'createUser',
+ },
+ {
+ title: 'App',
+ dataIndex: 'createAppId',
+ key: 'createAppId',
+ },
+ {
+ title: formatMessage({ id: 'flow.operation' }),
+ dataIndex: 'id',
+ key: 'operation',
+ fixed: 'right',
+ render: (id: number, item: FlowModel) => {
+ return (
+
+
+
+ {renderButton(item)}
+
+
+ {
+ if (dispatch) {
+ dispatch({
+ type: 'flowList/delete',
+ payload: { id },
+ });
+ }
+ }}
+ okText={formatMessage({ id: 'flow.delete' })}
+ cancelText={formatMessage({ id: 'flow.cancel' })}
+ >
+
+
+
+
+ );
+ },
+ },
+ ];
+ function handleMenuClick(e: ClickParam) {
+ if (dispatch) {
+ switch (e.key) {
+ case '1':
+ notification.info({ message: formatMessage({ id: 'flow.batch.fire' }) + batchId });
+ batchId.forEach((id: number | string) => {
+ dispatch({
+ type: 'flowList/start',
+ payload: {
+ id,
+ },
+ });
+ });
+ break;
+ case '2':
+ notification.info({ message: formatMessage({ id: 'flow.batch.begin' }) + batchId });
+ batchId.forEach((id: number | string) => {
+ dispatch({
+ type: 'flowList/scheduling',
+ payload: {
+ id,
+ },
+ });
+ });
+ break;
+ case '3':
+ notification.info({ message: formatMessage({ id: 'flow.batch.pause' }) + batchId });
+ batchId.forEach((id: number | string) => {
+ dispatch({
+ type: 'flowList/pause',
+ payload: {
+ id,
+ },
+ });
+ });
+ break;
+ case '4':
+ notification.info({ message: formatMessage({ id: 'flow.batch.delete' }) + batchId });
+ batchId.forEach((id: number | string) => {
+ dispatch({
+ type: 'flowList/delete',
+ payload: {
+ id,
+ },
+ });
+ });
+ break;
+ default:
+ }
+ setBatchId([]);
+ }
+ }
+ const menu = (
+
+ );
+ const rowSelection = {
+ selectedRowKeys: batchId,
+ onChange: (selectedRowKeys: string[] | number[], selectedRows: FlowModel[]) => {
+ setBatchId(selectedRowKeys);
+ },
+ };
+ return (
+
+
+
+
record.id}
+ dataSource={data}
+ pagination={{
+ showSizeChanger: true,
+ pageSize,
+ total: count,
+ current: page,
+ }}
+ onChange={tableChange}
+ loading={loading}
+ />
+
+ );
+};
+
+export default connect(({ flowList, loading }: ConnectState) => ({
+ tableResult: flowList.tableResult,
+ loading: loading.models.flowList,
+}))(FlowTable);
diff --git a/thain-fe/src/pages/Flow/List/SearchForm.tsx b/thain-fe/src/pages/Flow/List/SearchForm.tsx
new file mode 100644
index 00000000..40dcb751
--- /dev/null
+++ b/thain-fe/src/pages/Flow/List/SearchForm.tsx
@@ -0,0 +1,207 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { Button, Col, DatePicker, Form, Icon, Input, Row, Select } from 'antd';
+import React, { ChangeEvent, useState } from 'react';
+import styles from './TableList.less';
+import { connect } from 'dva';
+import { ConnectProps, ConnectState } from '@/models/connect';
+import { FlowListModelState } from './model';
+import { FlowLastRunStatusGetEntries } from '@/enums/FlowLastRunStatus';
+import { FlowSchedulingStatusGetEntries } from '@/enums/FlowSchedulingStatus';
+import { RangePickerPresetRange } from 'antd/lib/date-picker/interface';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+const { Option } = Select;
+const { RangePicker } = DatePicker;
+interface Props extends ConnectProps<{ flowId: number }> {
+ flowList: FlowListModelState;
+}
+const SearchForm: React.FC = ({
+ flowList: { flowId, lastRunStatus, flowName, scheduleStatus, updateTime, searchApp, createUser },
+ dispatch,
+}) => {
+ const [formFlowId, setFormFlowId] = useState(flowId);
+ const [formLastRunStatus, setFormLastRunStatus] = useState(lastRunStatus);
+ const [formFlowName, setFormFlowName] = useState(flowName);
+ const [formSearchApp, setFormSearchApp] = useState(searchApp);
+ const [formCreateUser, setFormCreateUser] = useState(createUser);
+ const [formScheduleStatus, setFormScheduleStatus] = useState(scheduleStatus);
+ const [formUpdateTime, setFormUpdateTime] = useState(updateTime);
+ const [showMore, setShowMore] = useState(false);
+
+ function changeFormFlowId(event: ChangeEvent) {
+ setFormFlowId(Number(event.target.value) || undefined);
+ }
+
+ function changeFormLastRunStatus(value: number) {
+ setFormLastRunStatus(value);
+ }
+
+ function changeFormFlowName(event: ChangeEvent) {
+ setFormFlowName(String(event.target.value) || undefined);
+ }
+
+ function changeFormSearchApp(event: ChangeEvent) {
+ setFormSearchApp(String(event.target.value) || undefined);
+ }
+
+ function changeFormCreateUser(event: ChangeEvent) {
+ setFormCreateUser(String(event.target.value) || undefined);
+ }
+
+ function changeFormScheduleStatus(value: number) {
+ setFormScheduleStatus(value);
+ }
+
+ function changeFormUpdateTime(dates: RangePickerPresetRange) {
+ setFormUpdateTime([dates[0] && dates[0].unix(), dates[1] && dates[1].unix()]);
+ }
+
+ function searchSubmit() {
+ if (dispatch) {
+ dispatch({
+ type: 'flowList/fetchTable',
+ payload: {
+ flowId: formFlowId || 0,
+ lastRunStatus: formLastRunStatus || 0,
+ flowName: formFlowName || '',
+ scheduleStatus: formScheduleStatus || 0,
+ updateTime: formUpdateTime || [],
+ searchApp: formSearchApp || '',
+ createUser: formCreateUser || '',
+ },
+ });
+ }
+ }
+ return (
+
+ );
+};
+
+export default connect(({ flowList }: ConnectState) => ({
+ flowList,
+}))(SearchForm);
diff --git a/thain-fe/src/pages/Flow/List/TableList.less b/thain-fe/src/pages/Flow/List/TableList.less
new file mode 100644
index 00000000..556d535a
--- /dev/null
+++ b/thain-fe/src/pages/Flow/List/TableList.less
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+@import '~antd/lib/style/themes/default.less';
+@import '~@/utils/utils.less';
+
+.tableList {
+ .tableListOperator {
+ margin-bottom: 16px;
+
+ button {
+ margin-right: 8px;
+ }
+ }
+}
+
+.tableListForm {
+ :global {
+ .ant-form-item {
+ display: flex;
+ margin-right: 0;
+ margin-bottom: 24px;
+
+ > .ant-form-item-label {
+ width: auto;
+ padding-right: 8px;
+ line-height: 32px;
+ }
+
+ .ant-form-item-control {
+ line-height: 32px;
+ }
+ }
+
+ .ant-form-item-control-wrapper {
+ flex: 1;
+ }
+ }
+
+ .submitButtons {
+ display: block;
+ margin-bottom: 24px;
+ white-space: nowrap;
+ }
+}
+
+@media screen and (max-width: @screen-lg) {
+ .tableListForm :global(.ant-form-item) {
+ margin-right: 24px;
+ }
+}
+
+@media screen and (max-width: @screen-md) {
+ .tableListForm :global(.ant-form-item) {
+ margin-right: 8px;
+ }
+}
diff --git a/thain-fe/src/pages/Flow/List/index.tsx b/thain-fe/src/pages/Flow/List/index.tsx
new file mode 100644
index 00000000..e4aa656b
--- /dev/null
+++ b/thain-fe/src/pages/Flow/List/index.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { PageHeaderWrapper } from '@ant-design/pro-layout';
+import { Card } from 'antd';
+import React, { useEffect } from 'react';
+import FlowTable from './FlowTable';
+import SearchForm from './SearchForm';
+import { connect } from 'dva';
+import { ConnectProps } from '@/models/connect';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+interface Props extends ConnectProps<{ flowId: number }> {}
+const FlowList: React.FC = ({ dispatch }) => {
+ useEffect(() => {
+ if (dispatch) {
+ dispatch({
+ type: 'flowList/fetchTable',
+ });
+ }
+ return () => {
+ if (dispatch) {
+ dispatch({
+ type: 'flowList/unmount',
+ });
+ }
+ };
+ });
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export default connect(() => ({}))(FlowList);
diff --git a/thain-fe/src/pages/Flow/List/model.ts b/thain-fe/src/pages/Flow/List/model.ts
new file mode 100644
index 00000000..debb4fbc
--- /dev/null
+++ b/thain-fe/src/pages/Flow/List/model.ts
@@ -0,0 +1,180 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+/**
+ * flow list 的model
+ */
+
+import { Effect } from 'dva';
+import { Reducer } from 'redux';
+import {
+ deleteFlow,
+ getTableList,
+ pauseFlow,
+ schedulingFlow,
+ startFlow,
+} from '@/pages/Flow/List/service';
+import { ConnectState } from '@/models/connect';
+import { TableResult } from '@/typings/ApiResult';
+import { FlowModel } from '@/commonModels/FlowModel';
+import { FlowSchedulingStatus } from '@/enums/FlowSchedulingStatus';
+import { notification } from 'antd';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+export interface FetchTableData {
+ flowId?: number;
+ page?: number;
+ pageSize?: number;
+ lastRunStatus?: number;
+}
+
+export class FlowListModelState {
+ flowId?: number;
+ lastRunStatus?: number;
+ flowName?: string;
+ searchApp?: string;
+ createUser?: string;
+ scheduleStatus?: FlowSchedulingStatus;
+ updateTime?: number[];
+ tableResult = new TableResult();
+}
+
+interface FlowListModelType {
+ namespace: 'flowList';
+ state: FlowListModelState;
+ effects: {
+ fetchTable: Effect;
+ scheduling: Effect;
+ pause: Effect;
+ start: Effect;
+ delete: Effect;
+ };
+ reducers: {
+ updateState: Reducer;
+ unmount: Reducer;
+ };
+}
+
+export interface FlowSearch {
+ flowId?: number;
+ lastRunStatus?: number;
+ flowName?: string;
+ searchApp?: string;
+ createUser?: string;
+ scheduleStatus?: FlowSchedulingStatus;
+ updateTime?: number[];
+ page?: number;
+ pageSize?: number;
+ sortKey?: string;
+ sortOrderDesc?: boolean;
+}
+
+const FlowListModel: FlowListModelType = {
+ namespace: 'flowList',
+ state: new FlowListModelState(),
+
+ effects: {
+ *fetchTable({ payload }, { call, put, select }) {
+ const state: FlowListModelState = yield select((s: ConnectState) => s.flowList);
+ const props: FlowSearch = {
+ flowId: payload && payload.flowId !== undefined ? payload.flowId : state.flowId,
+ lastRunStatus:
+ payload && payload.lastRunStatus !== undefined
+ ? payload.lastRunStatus
+ : state.lastRunStatus,
+ page: (payload && payload.page) || state.tableResult.page,
+ pageSize: (payload && payload.pageSize) || state.tableResult.pageSize,
+ sortKey: (payload && payload.sort && payload.sort.key) || 'id',
+ sortOrderDesc: (payload && payload.sort && payload.sort.orderDesc) || false,
+ flowName: payload && payload.flowName !== undefined ? payload.flowName : state.flowName,
+ searchApp: payload && payload.searchApp !== undefined ? payload.searchApp : state.searchApp,
+ createUser:
+ payload && payload.createUser !== undefined ? payload.createUser : state.createUser,
+ scheduleStatus:
+ payload && payload.scheduleStatus !== undefined
+ ? payload.scheduleStatus
+ : state.scheduleStatus,
+ updateTime:
+ payload && payload.updateTime !== undefined ? payload.updateTime : state.updateTime,
+ };
+ const tableResult: TableResult | undefined = yield call(getTableList, props);
+ yield put({
+ type: 'updateState',
+ payload: {
+ tableResult,
+ flowId: props.flowId,
+ lastRunStatus: props.lastRunStatus,
+ flowName: props.flowName,
+ scheduleStatus: props.scheduleStatus,
+ updateTime: props.updateTime,
+ searchApp: props.searchApp,
+ createUser: props.createUser,
+ },
+ });
+ },
+ *scheduling({ payload: { id } }, { call, put }) {
+ const result = yield call(schedulingFlow, id);
+ if (result === undefined) {
+ return;
+ }
+ notification.success({
+ message: `${id}:${formatMessage({ id: 'flow.begin.schedule.success' })}`,
+ });
+ yield put({
+ type: 'fetchTable',
+ });
+ },
+ *pause({ payload: { id } }, { call, put }) {
+ const result = yield call(pauseFlow, id);
+ if (result === undefined) {
+ return;
+ }
+ notification.success({
+ message: `${id}:${formatMessage({ id: 'flow.pause.schedule.success' })}`,
+ });
+ yield put({
+ type: 'fetchTable',
+ });
+ },
+ *start({ payload: { id } }, { call, put }) {
+ const result = yield call(startFlow, id);
+ if (result === undefined) {
+ return;
+ }
+ notification.success({
+ message: `${id}:${formatMessage({ id: 'flow.fire.success' })}`,
+ });
+ yield put({
+ type: 'fetchTable',
+ });
+ },
+ *delete({ payload: { id } }, { call, put }) {
+ const result = yield call(deleteFlow, id);
+ if (result === undefined) {
+ return;
+ }
+ notification.success({
+ message: `${id}:${formatMessage({ id: 'flow.delete.success' })}`,
+ });
+ yield put({
+ type: 'fetchTable',
+ });
+ },
+ },
+
+ reducers: {
+ updateState(state, action) {
+ return {
+ ...state,
+ ...action.payload,
+ };
+ },
+ unmount() {
+ return new FlowListModelState();
+ },
+ },
+};
+
+export default FlowListModel;
diff --git a/thain-fe/src/pages/Flow/List/service.ts b/thain-fe/src/pages/Flow/List/service.ts
new file mode 100644
index 00000000..8ad565ee
--- /dev/null
+++ b/thain-fe/src/pages/Flow/List/service.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { del, get, patch } from '@/utils/request';
+import { TableResult } from '@/typings/ApiResult';
+import { FlowModel } from '@/commonModels/FlowModel';
+import { FlowSearch } from './model';
+
+export async function getTableList(props: FlowSearch) {
+ return get>('/api/flow/list', props);
+}
+
+export async function deleteFlow(flowId: number) {
+ return del('/api/flow/' + flowId);
+}
+
+export async function startFlow(flowId: number) {
+ return patch('/api/flow/start/' + flowId);
+}
+
+export async function schedulingFlow(flowId: number) {
+ return patch('/api/flow/scheduling/' + flowId);
+}
+
+export async function pauseFlow(flowId: number) {
+ return patch('/api/flow/pause/' + flowId);
+}
diff --git a/thain-fe/src/pages/FlowEditor/EditorEdge.ts b/thain-fe/src/pages/FlowEditor/EditorEdge.ts
new file mode 100644
index 00000000..a285b7a5
--- /dev/null
+++ b/thain-fe/src/pages/FlowEditor/EditorEdge.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export class EditorEdge {
+ public id: string = '';
+ public index: number = 0;
+ public readonly shape = 'flow-smoot';
+ public source: string = '';
+ public target: string = '';
+ public sourceAnchor: number = 0;
+ public targetAnchor: number = 0;
+}
diff --git a/thain-fe/src/pages/FlowEditor/EditorFlowEntity.ts b/thain-fe/src/pages/FlowEditor/EditorFlowEntity.ts
new file mode 100644
index 00000000..60014880
--- /dev/null
+++ b/thain-fe/src/pages/FlowEditor/EditorFlowEntity.ts
@@ -0,0 +1,236 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+import { EditorEdge } from './EditorEdge';
+import { EditorNode } from './EditorNode';
+import { FlowAttributes } from '@/pages/FlowEditor/model';
+import { JobModel } from '@/commonModels/JobModel';
+import { FlowModel } from '@/commonModels/FlowModel';
+
+export class EditorFlowEntity {
+ public static getInstance(allInfo: { jobModelList: JobModel[]; flowModel?: FlowModel }) {
+ const { jobModelList, flowModel } = allInfo;
+ const instance = new EditorFlowEntity();
+ if (flowModel) {
+ instance.id = flowModel.id;
+ instance.name = flowModel.name;
+ instance.cron = flowModel.cron;
+ instance.createUser = flowModel.createUser;
+ instance.callbackEmail = flowModel.callbackEmail;
+ instance.callbackUrl = flowModel.callbackUrl;
+ instance.slaDuration = flowModel.slaDuration;
+ instance.slaEmail = flowModel.slaEmail;
+ instance.slaKill = flowModel.slaKill;
+ }
+ instance.jobs = jobModelList;
+ instance.needArrange = instance.getNeedArrange();
+ instance.initNodes();
+ instance.initEdges();
+ return instance;
+ }
+
+ private static getAnchor(sourceX: number, sourceY: number, targerX: number, targetY: number) {
+ const deltaX = Math.abs(sourceX - targerX);
+ const deltaY = Math.abs(sourceY - targetY);
+ if (deltaX > deltaY) {
+ if (sourceX > targerX) {
+ return [3, 1];
+ }
+ return [1, 3];
+ }
+ if (sourceY > targetY) {
+ return [0, 2];
+ }
+ return [2, 0];
+ }
+
+ public id?: number;
+ public name = '';
+ public cron?: string;
+ public createUser?: string;
+ public callbackEmail?: string;
+ public callbackUrl?: string;
+ public slaDuration?: number;
+ public slaEmail?: string;
+ public slaKill?: boolean;
+ public jobs: JobModel[] = [];
+ public editorNodes: EditorNode[] = [];
+ public editorEdges: EditorEdge[] = [];
+
+ private nameToJob = new Map();
+
+ private needArrange = false;
+
+ public getAttributes(): FlowAttributes {
+ return {
+ name: this.name,
+ cron: this.cron,
+ callbackEmail: this.callbackEmail,
+ callbackUrl: this.callbackUrl,
+ slaDuration: this.slaDuration,
+ slaEmail: this.slaEmail,
+ slaKill: this.slaKill,
+ };
+ }
+ /**
+ * 返回是否需要被安排位置
+ */
+ private getNeedArrange() {
+ for (let i = 0; i < this.jobs.length; i++) {
+ const job = this.jobs[i];
+ if (job.xAxis === undefined || job.xAxis <= 0 || job.yAxis === undefined || job.yAxis <= 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private initNodes() {
+ const result: EditorNode[] = [];
+ const nameToSourceNames = new Map();
+ for (let i = 0; i < this.jobs.length; i++) {
+ const editorNode = new EditorNode();
+ const job = this.jobs[i];
+ editorNode.jobId = job.id;
+ editorNode.id = job.name;
+ editorNode.category = job.component;
+ editorNode.index = i;
+ editorNode.label = job.name;
+
+ editorNode.x = job.xAxis;
+ editorNode.y = job.yAxis;
+ editorNode.attributes = job.properties;
+ editorNode.condition = job.condition;
+
+ const sourceJobNames = job.condition
+ .split(/&&|\|\|/)
+ .map(t => t.trim())
+ .map(t => {
+ const end = t.indexOf('.');
+ if (end < 0) {
+ return t;
+ }
+ return t.substring(0, end);
+ });
+ nameToSourceNames.set(job.name, sourceJobNames);
+ result.push(editorNode);
+ }
+ /**
+ * 节点名称和对应的网格
+ */
+ const nameToCoordinate = new Map();
+ const map: boolean[][] = [];
+
+ const getRow = (col: number, minRow: number) => {
+ while (true) {
+ if (map[col] === undefined) {
+ map[col] = [];
+ }
+ if (!map[col][minRow]) {
+ map[col][minRow] = true;
+ return minRow;
+ }
+ minRow++;
+ }
+ };
+ const deal = (name: string) => {
+ if (nameToCoordinate.get(name)) {
+ return true;
+ }
+ const sources = nameToSourceNames.get(name);
+
+ if (sources === undefined) {
+ return true;
+ }
+ let [col, row] = [0, 0];
+ for (const source of sources) {
+ if (source === '') {
+ continue;
+ }
+ const coo = nameToCoordinate.get(source);
+ if (coo === undefined) {
+ deal(source);
+ return false;
+ }
+ col = Math.max(col, coo[0] + 1);
+ row = Math.max(row, coo[1]);
+ }
+ nameToCoordinate.set(name, [col, getRow(col, row)]);
+ return true;
+ };
+ while (true) {
+ let canBreak = true;
+ for (let i = 0; i < this.jobs.length; i++) {
+ const name = this.jobs[i].name;
+ const flag = deal(name);
+ if (!flag) {
+ canBreak = false;
+ }
+ }
+ if (canBreak) {
+ break;
+ }
+ }
+
+ if (this.needArrange) {
+ for (const editorNode of result) {
+ const coo = nameToCoordinate.get(editorNode.id);
+ if (coo === undefined) {
+ continue;
+ }
+ editorNode.x = coo[0] * 110 + 200;
+ editorNode.y = coo[1] * 60 + 100;
+ }
+ }
+
+ this.editorNodes = result;
+ }
+
+ private initEdges() {
+ if (this.nameToJob.size === 0) {
+ for (const job of this.jobs) {
+ this.nameToJob.set(job.name, job);
+ }
+ }
+ const result: EditorEdge[] = [];
+ let index = this.jobs.length;
+ for (const job of this.jobs) {
+ if (!job.condition) {
+ continue;
+ }
+ const sourceJobNames = job.condition
+ .split(/&&|\|\|/)
+ .map(t => t.trim())
+ .map(t => {
+ const end = t.indexOf('.');
+ if (end < 0) {
+ return t;
+ }
+ return t.substring(0, end);
+ });
+
+ for (const sourceJobName of sourceJobNames) {
+ if (sourceJobName === job.name) {
+ continue;
+ }
+ const sourceJob = this.nameToJob.get(sourceJobName);
+ if (!sourceJob) {
+ continue;
+ }
+ const editorEdge = new EditorEdge();
+ editorEdge.id = index + '';
+ editorEdge.index = index++;
+ editorEdge.source = sourceJobName;
+ editorEdge.target = job.name;
+
+ [editorEdge.sourceAnchor, editorEdge.targetAnchor] = this.needArrange
+ ? [1, 3]
+ : EditorFlowEntity.getAnchor(sourceJob.xAxis, sourceJob.yAxis, job.xAxis, job.yAxis);
+ result.push(editorEdge);
+ }
+ }
+ this.editorEdges = result;
+ }
+}
diff --git a/thain-fe/src/pages/FlowEditor/EditorNode.ts b/thain-fe/src/pages/FlowEditor/EditorNode.ts
new file mode 100644
index 00000000..c137d70c
--- /dev/null
+++ b/thain-fe/src/pages/FlowEditor/EditorNode.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+export class EditorNode {
+ public id = '';
+ public jobId?: number;
+ public index = 0;
+ public attributes: { [props: string]: string } = {};
+ public condition = '';
+ public callbackUrl = '';
+ public category = '';
+ public color = '#1890ff';
+ public label = '';
+ public readonly shape = 'flow-rect';
+ public readonly size = '80*48';
+ public readonly type = 'node';
+ public x = 0;
+ public y = 0;
+ public logs = '';
+}
diff --git a/thain-fe/src/pages/FlowEditor/components/Contextmenu.tsx b/thain-fe/src/pages/FlowEditor/components/Contextmenu.tsx
new file mode 100644
index 00000000..8e7c9df5
--- /dev/null
+++ b/thain-fe/src/pages/FlowEditor/components/Contextmenu.tsx
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+/**
+ * @date 2019年03月19日
+ * @author liangyongrui@xiaomi.com
+ */
+import React, { useRef, useEffect } from 'react';
+import Editor from '../editor';
+import '../style/contextmenu.less';
+import { connect } from 'dva';
+import { ConnectState, ConnectProps } from '@/models/connect';
+
+interface Props extends ConnectProps<{ flowId: number }> {
+ readonly editor?: Editor;
+}
+
+const Contextmenu: React.FC = ({ editor }) => {
+ const contextmenuContainer = useRef(null);
+
+ useEffect(() => {
+ if (editor) {
+ const contextmenu = new Editor.Contextmenu({ container: contextmenuContainer.current });
+ editor.add(contextmenu);
+ }
+ }, [editor]);
+
+ return (
+
+
+
+ 复制
+ copy
+
+
+ 删除
+ delete
+
+
+
+
+
+ 复制
+ copy
+
+
+ 删除
+ delete
+
+
+ 解组
+ unGroup
+
+
+
+
+ 撤销
+ undo
+
+
+ 重做
+ redo
+
+
+ 粘贴
+ pasteHere
+
+
+
+
+ 复制
+ copy
+
+
+ 粘贴
+ paste
+
+
+ 归组
+ addGroup
+
+
+ 删除
+ delete
+
+
+
+ );
+};
+
+export default connect(({ flowEditor }: ConnectState) => ({
+ ...flowEditor,
+}))(Contextmenu);
diff --git a/thain-fe/src/pages/FlowEditor/components/FlowDetailPanel.tsx b/thain-fe/src/pages/FlowEditor/components/FlowDetailPanel.tsx
new file mode 100644
index 00000000..1e4e1d5e
--- /dev/null
+++ b/thain-fe/src/pages/FlowEditor/components/FlowDetailPanel.tsx
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ *
+ * date 2019年03月19日
+ * @author liangyongrui@xiaomi.com
+ */
+import Editor from '../editor';
+import React, { useState } from 'react';
+import { Checkbox, Modal, TimePicker, Form, Input } from 'antd';
+import { CheckboxChangeEvent } from 'antd/lib/checkbox';
+import ConnectState, { ConnectProps } from '@/models/connect';
+import { connect } from 'dva';
+import { FlowAttributes } from '@/pages/FlowEditor/model';
+import LineInput from './input/LineInput';
+import TextareaInput from './input/TextareaInput';
+import Button from 'antd/es/button/button';
+import moment, { Moment } from 'moment';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+function getDaySeconds(time: Moment | undefined) {
+ if (!time) {
+ return 0;
+ }
+ return time.second() + time.minute() * 60 + time.hour() * 60 * 60;
+}
+
+function daySecondsToMoment(second: number | undefined) {
+ if (!second) {
+ return moment({ second: 0, minute: 0, hour: 3 });
+ }
+ const s = second % 60;
+ const m = Math.floor(second / 60) % 60;
+ const h = Math.floor(second / 60 / 60) % 24;
+ return moment({ second: s, minute: m, hour: h });
+}
+
+interface Props extends ConnectProps<{ flowId: number }> {
+ readonly editor?: Editor;
+ updateGraph: (key: string, value: string, updateAttributes?: boolean) => void;
+ flowAttributes: FlowAttributes;
+ flowId: number;
+}
+const DetailPanel: React.FC = ({
+ editor,
+ flowAttributes,
+ flowId,
+ updateGraph,
+ dispatch,
+}) => {
+ if (flowId && !flowAttributes.name) {
+ return ;
+ }
+ const onBlurFunction = (attr: string, value: any) => {
+ if (dispatch) {
+ dispatch({
+ type: 'flowEditor/changeFlowAttributes',
+ payload: {
+ [attr]: value,
+ },
+ });
+ }
+ };
+
+ function toggleGrid(e: CheckboxChangeEvent) {
+ const page = editor!.getCurrentPage();
+ if (e.target.checked) {
+ page.showGrid();
+ } else {
+ page.hideGrid();
+ }
+ }
+
+ const [slaModalShow, setSlaModalShow] = useState(false);
+ const [slaTime, setSlaTime] = useState(daySecondsToMoment(flowAttributes.slaDuration));
+ const [slaEmail, setSlaEmail] = useState(flowAttributes.slaEmail);
+
+ return (
+
+
+
+ {formatMessage({ id: 'flow.grid.align' })}
+
+
+ {formatMessage({ id: 'flow.name' })} *
+
+
+
+ {formatMessage({ id: 'flow.cron' })}
+
+
+
+ {formatMessage({ id: 'flow.status.callback.url' })}
+
+
+
+ {formatMessage({ id: 'flow.failure.alarm.mail' })}
+
+
+
+
+
+
setSlaModalShow(false)}
+ onOk={() => {
+ if (dispatch) {
+ dispatch({
+ type: 'flowEditor/changeFlowAttributes',
+ payload: {
+ slaDuration: getDaySeconds(slaTime),
+ slaEmail,
+ slaKill: true,
+ },
+ callback: () => setSlaModalShow(false),
+ });
+ }
+ }}
+ >
+
+ setSlaTime(t)} />
+
+
+ setSlaEmail(t.currentTarget.value)}
+ />
+
+
+
+
+ );
+};
+
+export default connect(({ flowEditor }: ConnectState) => ({
+ ...flowEditor,
+}))(DetailPanel);
diff --git a/thain-fe/src/pages/FlowEditor/components/ItemPanel.tsx b/thain-fe/src/pages/FlowEditor/components/ItemPanel.tsx
new file mode 100644
index 00000000..5f49518c
--- /dev/null
+++ b/thain-fe/src/pages/FlowEditor/components/ItemPanel.tsx
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ * @date 2019年03月19日
+ * @author liangyongrui@xiaomi.com
+ */
+import Editor from '../editor';
+import React, { useRef, useEffect } from 'react';
+import '../style/itempanel.less';
+import { connect } from 'dva';
+import { ConnectState, ConnectProps } from '@/models/connect';
+import { ComponentDefineJsons } from '@/typings/entity/ComponentDefineJsons';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+const nodeCommonDefine = {
+ src: require('../item-icon/node.svg'),
+ 'data-size': '80*48',
+ 'data-shape': 'flow-rect',
+ 'data-color': '#1890FF',
+ 'data-type': 'node',
+ 'data-attributes': '',
+ draggable: false,
+};
+interface Props extends ConnectProps<{ flowId: number }> {
+ readonly editor?: Editor;
+ componentDefines: ComponentDefineJsons;
+}
+
+const ItemPanel: React.FC = ({ editor, componentDefines }) => {
+ const itemPanelContainer = useRef(null);
+ useEffect(() => {
+ if (editor) {
+ const itemPanel = new Editor.Itempanel({ container: itemPanelContainer.current });
+ editor.add(itemPanel);
+ }
+ }, [editor]);
+
+ const contain: any[] = [];
+ for (const componentName of Object.keys(componentDefines)) {
+ contain.push(
+
+

+
+ {componentName}
+
+
,
+ );
+ }
+
+ return (
+
+ {contain}
+
+ );
+};
+
+export default connect(({ flowEditor }: ConnectState) => ({
+ ...flowEditor,
+}))(ItemPanel);
diff --git a/thain-fe/src/pages/FlowEditor/components/Navigator.tsx b/thain-fe/src/pages/FlowEditor/components/Navigator.tsx
new file mode 100644
index 00000000..340094b1
--- /dev/null
+++ b/thain-fe/src/pages/FlowEditor/components/Navigator.tsx
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ */
+/**
+ * @date 2019年03月19日
+ * @author liangyongrui@xiaomi.com
+ */
+import React, { useRef, useEffect } from 'react';
+import Editor from '../editor';
+import '../style/navigator.less';
+import ConnectState, { ConnectProps } from '@/models/connect';
+import { connect } from 'dva';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+interface Props extends ConnectProps<{ flowId: number }> {
+ readonly editor?: Editor;
+}
+
+const Navigator: React.FC = ({ editor }) => {
+ const miniMapContainer = useRef(null);
+ useEffect(() => {
+ if (editor) {
+ const miniMap = new Editor.Minimap({ container: miniMapContainer.current });
+ editor.add(miniMap);
+ }
+ }, [editor]);
+
+ return (
+
+
{formatMessage({ id: 'global.navigation' })}
+
+
+ );
+};
+
+export default connect(({ flowEditor }: ConnectState) => ({
+ ...flowEditor,
+}))(Navigator);
diff --git a/thain-fe/src/pages/FlowEditor/components/NodeDetail.tsx b/thain-fe/src/pages/FlowEditor/components/NodeDetail.tsx
new file mode 100644
index 00000000..0607306a
--- /dev/null
+++ b/thain-fe/src/pages/FlowEditor/components/NodeDetail.tsx
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ * @date 2019年03月27日
+ * @author liangyongrui@xiaomi.com
+ */
+import React from 'react';
+import ConnectState, { ConnectProps } from '@/models/connect';
+import { ComponentDefineJsons } from '@/typings/entity/ComponentDefineJsons';
+import { connect } from 'dva';
+import LineInput from './input/LineInput';
+import TextareaInput from './input/TextareaInput';
+import { Form } from 'antd';
+import RichTextInput from './input/RichTextInput';
+import SelectInput from './input/SelectInput';
+import SqlInput from './input/SqlInput';
+import ShellInput from './input/ShellInput';
+import { SelectedModel } from '../model';
+import UploadBase64Input from './input/UploadBase64Input';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+/**
+ *
+ * 点击节点展示的信息
+ */
+interface Props extends ConnectProps<{ flowId: number }> {
+ updateGraph: (key: string, value: string, updateAttributes?: boolean) => void;
+ componentDefines: ComponentDefineJsons;
+ selectedModel: SelectedModel;
+}
+
+const getInput = (inputName: string) => {
+ switch (inputName) {
+ case 'line':
+ return LineInput;
+ case 'richText':
+ return RichTextInput;
+ case 'select':
+ return SelectInput;
+ case 'sql':
+ return SqlInput;
+ case 'shell':
+ return ShellInput;
+ case 'textarea':
+ return TextareaInput;
+ case 'uploadBase64':
+ return UploadBase64Input;
+ default:
+ return LineInput;
+ }
+};
+
+const NodeDetail: React.FC = ({ selectedModel, componentDefines, updateGraph }) => {
+ const { category, id } = selectedModel;
+ const componentDefine = componentDefines[category];
+ if (!category) {
+ return ;
+ }
+
+ const otherDetail: JSX.Element[] = [];
+ componentDefine.forEach(item => {
+ const Input = getInput(item.input.id);
+ otherDetail.push(
+
+ ;
+ }).options
+ }
+ />
+ ,
+ );
+ });
+
+ const formItemLayout = {
+ labelCol: {
+ xs: { span: 24 },
+ sm: { span: 8 },
+ },
+ wrapperCol: {
+ xs: { span: 24 },
+ sm: { span: 16 },
+ },
+ };
+ return (
+ {category}
+
+ {
+ const begin = selectedModel.label.indexOf('::');
+ if (begin === -1) {
+ return selectedModel.label;
+ }
+ return selectedModel.label.substr(begin + 2);
+ })()}
+ />
+
+
+
+
+
+
+
+ {otherDetail}
+
+ );
+};
+export default connect(({ flowEditor }: ConnectState) => ({
+ ...flowEditor,
+}))(NodeDetail);
diff --git a/thain-fe/src/pages/FlowEditor/components/Page.tsx b/thain-fe/src/pages/FlowEditor/components/Page.tsx
new file mode 100644
index 00000000..8335e1c0
--- /dev/null
+++ b/thain-fe/src/pages/FlowEditor/components/Page.tsx
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2019, Xiaomi, Inc. All rights reserved.
+ * This source code is licensed under the Apache License Version 2.0, which
+ * can be found in the LICENSE file in the root directory of this source tree.
+ * date 2019年03月19日
+ * @author liangyongrui@xiaomi.com
+ */
+
+import Editor from '../editor';
+import React, { useRef, useEffect, useState } from 'react';
+import { connect } from 'dva';
+import ConnectState, { ConnectProps } from '@/models/connect';
+import { Modal } from 'antd';
+import NodeDetail from './NodeDetail';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+interface Props extends ConnectProps<{ flowId: number }> {
+ readonly editor?: Editor;
+ flowId: number;
+}
+
+const Page: React.FC = ({ editor, flowId, dispatch }) => {
+ const pageContainer = useRef(null);
+ const [jobVisible, setJobVisible] = useState(false);
+ function createPage(container: any): any {
+ const height = window.innerHeight - 118;
+ return new Editor.Flow({
+ graph: { container, height },
+ align: { grid: true },
+ });
+ }
+
+ useEffect(() => {
+ if (editor) {
+ const page = createPage(pageContainer.current);
+ page.changeAddEdgeModel({ shape: 'flow-smoot' });
+ page.getGraph().on('node:click', (ev: any) => {
+ if (dispatch) {
+ dispatch({
+ type: 'flowEditor/changeSelectedModel',
+ payload: {
+ selectedModel: ev.item.getModel(),
+ },
+ callback: () => setJobVisible(true),
+ });
+ }
+ });
+ editor.add(page);
+ if (dispatch && flowId) {
+ dispatch({
+ type: 'flowEditor/loadPage',
+ payload: {
+ flowId,
+ page,
+ },
+ });
+ }
+
+ document.onclick = () => {
+ // const currentPage = editor.getCurrentPage();
+ // const nodes: any[] = currentPage.getNodes();
+ // const edges: any[] = currentPage.getEdges();
+ // const nodeMap = nodes.reduce