diff --git a/ui/src/components/workflow/run/WorkflowRuns.tsx b/ui/src/components/workflow/run/WorkflowRuns.tsx
index 0cf76772..09b116da 100644
--- a/ui/src/components/workflow/run/WorkflowRuns.tsx
+++ b/ui/src/components/workflow/run/WorkflowRuns.tsx
@@ -4,7 +4,6 @@ import { useTranslation } from "react-i18next";
import { useRequest } from "ahooks";
import { Button, Empty, notification, Space, Table, theme, Tooltip, Typography, type TableProps } from "antd";
import { CircleCheck as CircleCheckIcon, CircleX as CircleXIcon, Eye as EyeIcon } from "lucide-react";
-import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase";
import WorkflowRunDetailDrawer from "./WorkflowRunDetailDrawer";
@@ -64,7 +63,7 @@ const WorkflowRuns = ({ className, style }: WorkflowRunsProps) => {
key: "startedAt",
title: t("workflow_run.props.started_at"),
ellipsis: true,
- render: (_, record) => {
+ render: () => {
return "TODO";
},
},
@@ -72,7 +71,7 @@ const WorkflowRuns = ({ className, style }: WorkflowRunsProps) => {
key: "completedAt",
title: t("workflow_run.props.completed_at"),
ellipsis: true,
- render: (_, record) => {
+ render: () => {
return "TODO";
},
},
diff --git a/ui/src/i18n/locales/en/nls.certificate.json b/ui/src/i18n/locales/en/nls.certificate.json
index d1146930..5422f0c2 100644
--- a/ui/src/i18n/locales/en/nls.certificate.json
+++ b/ui/src/i18n/locales/en/nls.certificate.json
@@ -4,6 +4,7 @@
"certificate.nodata": "No certificates. Please create a workflow to generate certificates! 😀",
"certificate.action.view": "View Certificate",
+ "certificate.action.delete": "Delete Certificate",
"certificate.action.download": "Download Certificate",
"certificate.props.san": "Name",
diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json
index 6f39393b..2fdb4002 100644
--- a/ui/src/i18n/locales/en/nls.workflow.json
+++ b/ui/src/i18n/locales/en/nls.workflow.json
@@ -21,7 +21,7 @@
"workflow.props.updated_at": "Updated At",
"workflow.detail.orchestration.tab": "Orchestration",
- "workflow.detail.runs.tab": "Workflow Runs",
+ "workflow.detail.runs.tab": "History Runs",
"workflow.baseinfo.modal.title": "Workflow Base Information",
"workflow.baseinfo.form.name.label": "Name",
@@ -29,6 +29,18 @@
"workflow.baseinfo.form.description.label": "Description",
"workflow.baseinfo.form.description.placeholder": "Please enter description",
+ "workflow.nodes.start.form.trigger.label": "Trigger",
+ "workflow.nodes.start.form.trigger.placeholder": "Please select trigger",
+ "workflow.nodes.start.form.trigger.tooltip": "Auto: Time triggered based on cron expression.
Manual: Manually triggered.",
+ "workflow.nodes.start.form.trigger.option.auto.label": "Auto",
+ "workflow.nodes.start.form.trigger.option.manual.label": "Manual",
+ "workflow.nodes.start.form.trigger_cron.label": "Cron Expression",
+ "workflow.nodes.start.form.trigger_cron.placeholder": "Please enter cron expression",
+ "workflow.nodes.start.form.trigger_cron.errmsg.invalid": "Please enter a valid cron expression",
+ "workflow.nodes.start.form.trigger_cron.tooltip": "Time zone is based on the server.",
+ "workflow.nodes.start.form.trigger_cron.extra": "Expected execution time for the last 5 times:",
+ "workflow.nodes.start.form.trigger_cron_alert.content": "Tips: If you have multiple workflows, it is recommended to set them to run at multiple times of the day instead of always running at specific times.
Reference links:
1.
Let’s Encrypt rate limits2.
Why should my Let’s Encrypt (ACME) client run at a random time?",
+
"workflow_run.props.id": "ID",
"workflow_run.props.status": "Status",
"workflow_run.props.status.succeeded": "Succeeded",
@@ -60,13 +72,6 @@
"workflow.node.addBranch.label": "Add Branch",
"workflow.node.selectNodeType.label": "Select Node Type",
- "workflow.node.start.form.executionMethod.label": "Execution Method",
- "workflow.node.start.form.executionMethod.placeholder": "Please select execution method",
- "workflow.node.start.form.executionMethod.options.manual": "Manual",
- "workflow.node.start.form.executionMethod.options.auto": "Auto",
- "workflow.node.start.form.crontab.label": "Crontab",
- "workflow.node.start.form.crontab.placeholder": "Please enter crontab",
-
"workflow.node.notify.form.title.label": "Title",
"workflow.node.notify.form.title.placeholder": "Please enter title",
"workflow.node.notify.form.content.label": "Content",
diff --git a/ui/src/i18n/locales/zh/nls.certificate.json b/ui/src/i18n/locales/zh/nls.certificate.json
index 1c4d3d24..0353b38d 100644
--- a/ui/src/i18n/locales/zh/nls.certificate.json
+++ b/ui/src/i18n/locales/zh/nls.certificate.json
@@ -4,6 +4,7 @@
"certificate.nodata": "暂无证书,新建一个工作流去生成证书吧~ 😀",
"certificate.action.view": "查看证书",
+ "certificate.action.delete": "删除证书",
"certificate.action.download": "下载证书",
"certificate.props.san": "名称",
diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json
index c2729c23..0d75ce85 100644
--- a/ui/src/i18n/locales/zh/nls.workflow.json
+++ b/ui/src/i18n/locales/zh/nls.workflow.json
@@ -29,6 +29,18 @@
"workflow.baseinfo.form.description.label": "描述",
"workflow.baseinfo.form.description.placeholder": "请输入工作流描述",
+ "workflow.nodes.start.form.trigger.label": "触发方式",
+ "workflow.nodes.start.form.trigger.placeholder": "请选择触发方式",
+ "workflow.nodes.start.form.trigger.tooltip": "自动触发:基于 Cron 表达式定时触发。
手动触发:手动点击执行触发。",
+ "workflow.nodes.start.form.trigger.option.auto.label": "自动触发",
+ "workflow.nodes.start.form.trigger.option.manual.label": "手动触发",
+ "workflow.nodes.start.form.trigger_cron.label": "Cron 表达式",
+ "workflow.nodes.start.form.trigger_cron.placeholder": "请输入 Cron 表达式",
+ "workflow.nodes.start.form.trigger_cron.errmsg.invalid": "请输入正确的 Cron 表达式",
+ "workflow.nodes.start.form.trigger_cron.tooltip": "时区以服务器设置为准。",
+ "workflow.nodes.start.form.trigger_cron.extra": "预计最近 5 次执行时间:",
+ "workflow.nodes.start.form.trigger_cron_alert.content": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。
参考链接:
1.
Let’s Encrypt 速率限制2.
为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?",
+
"workflow_run.props.id": "ID",
"workflow_run.props.status": "状态",
"workflow_run.props.status.succeeded": "成功",
@@ -60,13 +72,6 @@
"workflow.node.addBranch.label": "添加分支",
"workflow.node.selectNodeType.label": "选择节点类型",
- "workflow.node.start.form.executionMethod.label": "执行方式",
- "workflow.node.start.form.executionMethod.placeholder": "请选择执行方式",
- "workflow.node.start.form.executionMethod.options.manual": "手动",
- "workflow.node.start.form.executionMethod.options.auto": "自动",
- "workflow.node.start.form.crontab.label": "定时表达式",
- "workflow.node.start.form.crontab.placeholder": "请输入定时表达式",
-
"workflow.node.notify.form.title.label": "标题",
"workflow.node.notify.form.title.placeholder": "请输入标题",
"workflow.node.notify.form.content.label": "内容",
diff --git a/ui/src/pages/certificates/CertificateList.tsx b/ui/src/pages/certificates/CertificateList.tsx
index b2bb939c..b0517283 100644
--- a/ui/src/pages/certificates/CertificateList.tsx
+++ b/ui/src/pages/certificates/CertificateList.tsx
@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
import { useRequest } from "ahooks";
import { Button, Divider, Empty, Menu, notification, Radio, Space, Table, theme, Tooltip, Typography, type MenuProps, type TableProps } from "antd";
import { PageHeader } from "@ant-design/pro-components";
-import { Eye as EyeIcon, Filter as FilterIcon } from "lucide-react";
+import { Eye as EyeIcon, Filter as FilterIcon, Trash2 as Trash2Icon } from "lucide-react";
import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase";
@@ -162,6 +162,17 @@ const CertificateList = () => {
}
/>
+
+
+ }
+ variant="text"
+ onClick={() => {
+ alert("TODO");
+ }}
+ />
+
),
},
diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx
index 7770982c..0ae2df91 100644
--- a/ui/src/pages/workflows/WorkflowDetail.tsx
+++ b/ui/src/pages/workflows/WorkflowDetail.tsx
@@ -1,11 +1,12 @@
import { cloneElement, memo, useEffect, useMemo, useState } from "react";
-import { useParams } from "react-router-dom";
+import { useNavigate, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useDeepCompareEffect } from "ahooks";
-import { Button, Card, Form, Input, message, Modal, notification, Tabs, Typography, type FormInstance } from "antd";
+import { Button, Card, Dropdown, Form, Input, message, Modal, notification, Tabs, Typography, type FormInstance } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { PageHeader } from "@ant-design/pro-components";
import { z } from "zod";
+import { Ellipsis as EllipsisIcon, Trash2 as Trash2Icon } from "lucide-react";
import Show from "@/components/Show";
import End from "@/components/workflow/End";
@@ -15,13 +16,17 @@ import WorkflowProvider from "@/components/workflow/WorkflowProvider";
import { useZustandShallowSelector } from "@/hooks";
import { allNodesValidated, type WorkflowModel, type WorkflowNode } from "@/domain/workflow";
import { useWorkflowStore } from "@/stores/workflow";
+import { remove as removeWorkflow } from "@/repository/workflow";
import { run as runWorkflow } from "@/api/workflow";
import { getErrMsg } from "@/utils/error";
const WorkflowDetail = () => {
+ const navigate = useNavigate();
+
const { t } = useTranslation();
const [messageApi, MessageContextHolder] = message.useMessage();
+ const [modalApi, ModalContextHolder] = Modal.useModal();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
const { id: workflowId } = useParams();
@@ -30,7 +35,7 @@ const WorkflowDetail = () => {
);
useEffect(() => {
init(workflowId);
- }, [workflowId]);
+ }, [workflowId, init]);
const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration");
@@ -70,6 +75,24 @@ const WorkflowDetail = () => {
switchEnable();
};
+ const handleDeleteClick = () => {
+ modalApi.confirm({
+ title: t("workflow.action.delete"),
+ content: t("workflow.action.delete.confirm"),
+ onOk: async () => {
+ try {
+ const resp: boolean = await removeWorkflow(workflow);
+ if (resp) {
+ navigate("/workflows");
+ }
+ } catch (err) {
+ console.error(err);
+ notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
+ }
+ },
+ });
+ };
+
// const handleWorkflowSaveClick = () => {
// if (!allNodesValidated(workflow.draft as WorkflowNode)) {
// messageApi.warning(t("workflow.detail.action.save.failed.uncompleted"));
@@ -96,6 +119,7 @@ const WorkflowDetail = () => {
return (
{MessageContextHolder}
+ {ModalContextHolder}
{NotificationContextHolder}
@@ -104,19 +128,27 @@ const WorkflowDetail = () => {
title={workflow.name}
extra={[
-
- {t("common.button.edit")}
-
- }
- onFinish={handleBaseInfoFormFinish}
- />
+ {t("common.button.edit")}} onFinish={handleBaseInfoFormFinish} />
-
+
+
+ ,
+ onClick: () => {
+ handleDeleteClick();
+ },
+ },
+ ],
+ }}
+ >
+ } />
+
,
]}
>
diff --git a/ui/src/utils/cron.ts b/ui/src/utils/cron.ts
new file mode 100644
index 00000000..89db10b9
--- /dev/null
+++ b/ui/src/utils/cron.ts
@@ -0,0 +1,26 @@
+import { parseExpression } from "cron-parser";
+
+export const validCronExpression = (expr: string): boolean => {
+ try {
+ parseExpression(expr);
+ return true;
+ } catch {
+ return false;
+ }
+};
+
+export const getNextCronExecutions = (expr: string, times = 1): Date[] => {
+ try {
+ const now = new Date();
+ const cron = parseExpression(expr, { currentDate: now, iterator: true });
+
+ const result: Date[] = [];
+ for (let i = 0; i < times; i++) {
+ const next = cron.next();
+ result.push(next.value.toDate());
+ }
+ return result;
+ } catch {
+ return [];
+ }
+};