diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json
index 426a58f09..39acce5bf 100644
--- a/ui/src/i18n/locales/en/nls.workflow.json
+++ b/ui/src/i18n/locales/en/nls.workflow.json
@@ -21,16 +21,16 @@
"workflow.action.enable.button": "Activate",
"workflow.action.enable.errmsg.uncompleted": "Please complete the orchestration and publish the changes first",
"workflow.action.disable.button": "Deactivate",
- "workflow.action.release.button": "Release",
- "workflow.action.release.modal.title": "Release changes",
- "workflow.action.release.modal.content": "Are you sure to release your changes?",
- "workflow.action.release.errmsg.uncompleted": "Please complete the orchestration first",
- "workflow.action.discard.button": "Discard",
- "workflow.action.discard.modal.title": "Discard changes",
- "workflow.action.discard.modal.content": "Are you sure to discard your changes?",
+ "workflow.action.publish.button": "Publish",
+ "workflow.action.publish.modal.title": "Publish changes",
+ "workflow.action.publish.modal.content": "Are you sure to publish your changes?",
+ "workflow.action.publish.errmsg.uncompleted": "Please complete the orchestration first",
+ "workflow.action.rollback.button": "Rollback",
+ "workflow.action.rollback.modal.title": "Rollback changes",
+ "workflow.action.rollback.modal.content": "Are you sure to rollback your changes to the last published version?",
"workflow.action.run.button": "Run",
"workflow.action.run.modal.title": "Run workflow",
- "workflow.action.run.modal.content": "You have unreleased changes. Do you really want to run this workflow based on the latest released version?",
+ "workflow.action.run.modal.content": "You have unpublished changes. Do you really want to run this workflow based on the last published version?",
"workflow.action.run.prompt": "Running... Please check the history later",
"workflow.props.name": "Name",
@@ -55,18 +55,15 @@
"workflow.new.templates.template.certtest.description": "A monitoring operating procedure that includes monitoring, and notification steps.",
"workflow.new.templates.template.empty.title": "Empty template",
"workflow.new.templates.template.empty.description": "Customize all the contents of the workflow from the beginning.",
- "workflow.new.modal.title": "Create workflow",
- "workflow.new.modal.form.name.label": "Name",
- "workflow.new.modal.form.name.placeholder": "Please enter workflow name",
- "workflow.new.modal.form.description.label": "Description (Optional)",
- "workflow.new.modal.form.description.placeholder": "Please enter workflow description",
+ "workflow.new.templates.default_name": "Untitled workflow",
+ "workflow.new.templates.default_description": "Created at {{date}}",
"workflow.detail.baseinfo.modal.title": "Workflow base information",
"workflow.detail.baseinfo.form.name.label": "Name",
"workflow.detail.baseinfo.form.name.placeholder": "Please enter workflow name",
"workflow.detail.baseinfo.form.description.label": "Description (Optional)",
"workflow.detail.baseinfo.form.description.placeholder": "Please enter workflow description",
- "workflow.detail.orchestration.tab": "Orchestration",
- "workflow.detail.orchestration.draft.alert": "The orchestration is not released yet.",
+ "workflow.detail.design.tab": "Orchestration",
+ "workflow.detail.design.draft.alert": "The orchestration is not released yet.",
"workflow.detail.runs.tab": "History runs"
}
diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json
index ca74474b9..5ff7c4d2f 100644
--- a/ui/src/i18n/locales/zh/nls.workflow.json
+++ b/ui/src/i18n/locales/zh/nls.workflow.json
@@ -21,13 +21,13 @@
"workflow.action.enable.button": "启用",
"workflow.action.enable.errmsg.uncompleted": "请先完成流程编排并发布更改",
"workflow.action.disable.button": "停用",
- "workflow.action.release.button": "发布更改",
- "workflow.action.release.modal.title": "发布更改",
- "workflow.action.release.modal.content": "确定要发布更改吗?",
- "workflow.action.release.errmsg.uncompleted": "流程编排未完成,请检查是否有节点未配置",
- "workflow.action.discard.button": "撤销更改",
- "workflow.action.discard.modal.title": "撤销更改",
- "workflow.action.discard.modal.content": "确定要撤销更改并回退到最近一次发布的版本吗?",
+ "workflow.action.publish.button": "发布更改",
+ "workflow.action.publish.modal.title": "发布更改",
+ "workflow.action.publish.modal.content": "确定要发布更改吗?",
+ "workflow.action.publish.errmsg.uncompleted": "流程编排未完成,请检查是否有节点未配置",
+ "workflow.action.rollback.button": "回退更改",
+ "workflow.action.rollback.modal.title": "回退更改",
+ "workflow.action.rollback.modal.content": "确定要回退到最近一次发布的版本吗?",
"workflow.action.run.button": "执行",
"workflow.action.run.modal.title": "执行工作流",
"workflow.action.run.modal.content": "你有尚未发布的更改。确定要以最近一次发布的版本继续执行吗?",
@@ -55,18 +55,15 @@
"workflow.new.templates.template.certtest.description": "一个包含证书监控 + 消息通知步骤的工作流程。",
"workflow.new.templates.template.empty.title": "空白模板",
"workflow.new.templates.template.empty.description": "从零开始自定义工作流的任务内容。",
- "workflow.new.modal.title": "新建工作流",
- "workflow.new.modal.form.name.label": "名称",
- "workflow.new.modal.form.name.placeholder": "请输入工作流名称",
- "workflow.new.modal.form.description.label": "描述(可选)",
- "workflow.new.modal.form.description.placeholder": "请输入工作流描述",
+ "workflow.new.templates.default_name": "未命名工作流",
+ "workflow.new.templates.default_description": "于 {{date}} 创建",
"workflow.detail.baseinfo.modal.title": "编辑基本信息",
"workflow.detail.baseinfo.form.name.label": "名称",
"workflow.detail.baseinfo.form.name.placeholder": "请输入工作流名称",
"workflow.detail.baseinfo.form.description.label": "描述(可选)",
"workflow.detail.baseinfo.form.description.placeholder": "请输入工作流描述",
- "workflow.detail.orchestration.tab": "流程编排",
- "workflow.detail.orchestration.draft.alert": "当前编排有未发布的更改。",
+ "workflow.detail.design.tab": "流程编排",
+ "workflow.detail.design.draft.alert": "当前编排有未发布的更改。",
"workflow.detail.runs.tab": "执行历史"
}
diff --git a/ui/src/pages/settings/Settings.tsx b/ui/src/pages/settings/Settings.tsx
index 0c22af811..82fc6115b 100644
--- a/ui/src/pages/settings/Settings.tsx
+++ b/ui/src/pages/settings/Settings.tsx
@@ -16,17 +16,22 @@ const Settings = () => {
["ssl-provider", "settings.sslprovider.tab", ],
["persistence", "settings.persistence.tab", ],
] satisfies [string, string, React.ReactElement][];
- const [menuKey, setMenuValue] = useState("account");
+ const [menuKey, setMenuValue] = useState(menus[0][0]);
useEffect(() => {
- const path = location.pathname.split("/")[2];
- if (!path) {
+ const subpath = location.pathname.split("/")[2];
+ if (!subpath) {
navigate("/settings/account");
return;
}
- setMenuValue(path);
+ setMenuValue(subpath);
}, [location.pathname]);
+ const handleMenuClick = ({ key }: { key: string }) => {
+ setMenuValue(key);
+ navigate(`/settings/${key}`);
+ };
+
return (
@@ -46,10 +51,7 @@ const Settings = () => {
),
}))}
- onClick={({ key }) => {
- setMenuValue(key);
- navigate(`/settings/${key}`);
- }}
+ onClick={handleMenuClick}
/>
@@ -68,10 +70,7 @@ const Settings = () => {
),
}))}
- onClick={({ key }) => {
- setMenuValue(key);
- navigate(`/settings/${key}`);
- }}
+ onClick={handleMenuClick}
/>
diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx
index 66e404080..aad56f363 100644
--- a/ui/src/pages/workflows/WorkflowDetail.tsx
+++ b/ui/src/pages/workflows/WorkflowDetail.tsx
@@ -1,25 +1,21 @@
-import { useEffect, useMemo, useState } from "react";
+import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
-import { useNavigate, useParams } from "react-router-dom";
-import { IconArrowBackUp, IconChevronDown, IconDots, IconHistory, IconPlayerPlay, IconRobot, IconTrash } from "@tabler/icons-react";
-import { Alert, App, Button, Card, Dropdown, Flex, Form, Input, Segmented, Space } from "antd";
+import { Outlet, useLocation, useNavigate, useParams } from "react-router-dom";
+import { IconChevronDown, IconHistory, IconRobot, IconTrash } from "@tabler/icons-react";
+import { App, Button, Dropdown, Flex, Form, Input, Segmented, Skeleton } from "antd";
import { createSchemaFieldRule } from "antd-zod";
-import { isEqual } from "radash";
import { z } from "zod";
-import { startRun as startWorkflowRun } from "@/api/workflows";
import ModalForm from "@/components/ModalForm";
import Show from "@/components/Show";
-import WorkflowElementsContainer from "@/components/workflow/WorkflowElementsContainer";
-import WorkflowRuns from "@/components/workflow/WorkflowRuns";
import { isAllNodesValidated } from "@/domain/workflow";
-import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
import { useAntdForm, useZustandShallowSelector } from "@/hooks";
-import { remove as removeWorkflow, subscribe as subscribeWorkflow, unsubscribe as unsubscribeWorkflow } from "@/repository/workflow";
+import { remove as removeWorkflow } from "@/repository/workflow";
import { useWorkflowStore } from "@/stores/workflow";
import { getErrMsg } from "@/utils/error";
const WorkflowDetail = () => {
+ const location = useLocation();
const navigate = useNavigate();
const { t } = useTranslation();
@@ -27,9 +23,7 @@ const WorkflowDetail = () => {
const { message, modal, notification } = App.useApp();
const { id: workflowId } = useParams();
- const { workflow, initialized, ...workflowState } = useWorkflowStore(
- useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled", "release", "discard"])
- );
+ const { workflow, initialized, ...workflowState } = useWorkflowStore(useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled"]));
useEffect(() => {
workflowState.init(workflowId!);
@@ -38,43 +32,27 @@ const WorkflowDetail = () => {
};
}, [workflowId]);
- const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration");
-
- const [isPendingOrRunning, setIsPendingOrRunning] = useState(false);
- const lastRunStatus = useMemo(() => workflow.lastRunStatus, [workflow]);
-
- const [allowDiscard, setAllowDiscard] = useState(false);
- const [allowRelease, setAllowRelease] = useState(false);
- const [allowRun, setAllowRun] = useState(false);
-
+ const tabs = [
+ ["design", "workflow.detail.design.tab", ],
+ ["runs", "workflow.detail.runs.tab", ],
+ ] satisfies [string, string, React.ReactElement][];
+ const [tabValue, setTabValue] = useState(tabs[0][0]);
useEffect(() => {
- setIsPendingOrRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.PENDING || lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING);
- }, [lastRunStatus]);
-
- useEffect(() => {
- if (!!workflowId && isPendingOrRunning) {
- subscribeWorkflow(workflowId, (cb) => {
- if (cb.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.PENDING && cb.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) {
- setIsPendingOrRunning(false);
- unsubscribeWorkflow(workflowId);
- }
- });
-
- return () => {
- unsubscribeWorkflow(workflowId);
- };
+ const subpath = location.pathname.split("/")[3];
+ if (!tabs.map((tab) => tab[0]).includes(subpath)) {
+ navigate(`/workflows/${workflowId}/${tabs[0][0]}`, { replace: true });
+ return;
}
- }, [workflowId, isPendingOrRunning]);
- useEffect(() => {
- const hasReleased = !!workflow.content;
- const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content);
- setAllowDiscard(!isPendingOrRunning && hasReleased && hasChanges);
- setAllowRelease(!isPendingOrRunning && hasChanges);
- setAllowRun(hasReleased);
- }, [workflow.content, workflow.draft, workflow.hasDraft, isPendingOrRunning]);
+ setTabValue(subpath);
+ }, [location.pathname, workflowId]);
- const handleEnableChange = async () => {
+ const handleTabChange = (value: string) => {
+ setTabValue(value);
+ navigate(`/workflows/${workflowId}/${value}`);
+ };
+
+ const handleEnableClick = async () => {
if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) {
message.warning(t("workflow.action.enable.errmsg.uncompleted"));
return;
@@ -113,85 +91,6 @@ const WorkflowDetail = () => {
});
};
- const handleDiscardClick = () => {
- modal.confirm({
- title: t("workflow.action.discard.modal.title"),
- content: t("workflow.action.discard.modal.content"),
- onOk: async () => {
- try {
- await workflowState.discard();
-
- message.success(t("common.text.operation_succeeded"));
- } catch (err) {
- console.error(err);
- notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
- }
- },
- });
- };
-
- const handleReleaseClick = () => {
- if (!isAllNodesValidated(workflow.draft!)) {
- message.warning(t("workflow.action.release.errmsg.uncompleted"));
- return;
- }
-
- modal.confirm({
- title: t("workflow.action.release.modal.title"),
- content: t("workflow.action.release.modal.content"),
- onOk: async () => {
- try {
- await workflowState.release();
-
- message.success(t("common.text.operation_succeeded"));
- } catch (err) {
- console.error(err);
- notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
- }
- },
- });
- };
-
- const handleRunClick = () => {
- const { promise, resolve, reject } = Promise.withResolvers();
- if (workflow.hasDraft) {
- modal.confirm({
- title: t("workflow.action.run.modal.title"),
- content: t("workflow.action.run.modal.content"),
- onOk: () => resolve(void 0),
- onCancel: () => reject(),
- });
- } else {
- resolve(void 0);
- }
-
- promise.then(async () => {
- let unsubscribeFn: Awaited> | undefined = undefined;
-
- try {
- setIsPendingOrRunning(true);
-
- // subscribe before running workflow
- unsubscribeFn = await subscribeWorkflow(workflowId!, (e) => {
- if (e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.PENDING && e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) {
- setIsPendingOrRunning(false);
- unsubscribeFn?.();
- }
- });
-
- await startWorkflowRun(workflowId!);
-
- message.info(t("workflow.action.run.prompt"));
- } catch (err) {
- setIsPendingOrRunning(false);
- unsubscribeFn?.();
-
- console.error(err);
- message.warning(t("common.text.operation_failed"));
- }
- });
- };
-
return (
@@ -205,7 +104,7 @@ const WorkflowDetail = () => {
{initialized
? [
{t("common.button.edit")}} />,
-
-
-
-
-
-
-
- {t("workflow.detail.orchestration.draft.alert")}} showIcon type="warning" />
-
-
-
-
- } loading={isPendingOrRunning} type="primary" onClick={handleRunClick}>
- {t("workflow.action.run.button")}
-
-
-
- {t("workflow.action.release.button")}
-
- ,
- onClick: handleDiscardClick,
- },
- ],
- }}
- trigger={["click"]}
- >
- } variant="outlined" />
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
+ }
+ >
+
+
+
);
};
diff --git a/ui/src/pages/workflows/WorkflowDetailDesign.tsx b/ui/src/pages/workflows/WorkflowDetailDesign.tsx
new file mode 100644
index 000000000..1876a2ba5
--- /dev/null
+++ b/ui/src/pages/workflows/WorkflowDetailDesign.tsx
@@ -0,0 +1,193 @@
+import { useEffect, useMemo, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { IconArrowBackUp, IconDots, IconPlayerPlay } from "@tabler/icons-react";
+import { Alert, App, Button, Card, Dropdown, Space } from "antd";
+import { isEqual } from "radash";
+
+import { startRun as startWorkflowRun } from "@/api/workflows";
+import Show from "@/components/Show";
+import WorkflowElementsContainer from "@/components/workflow/WorkflowElementsContainer";
+import { isAllNodesValidated } from "@/domain/workflow";
+import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
+import { useZustandShallowSelector } from "@/hooks";
+import { subscribe as subscribeWorkflow, unsubscribe as unsubscribeWorkflow } from "@/repository/workflow";
+import { useWorkflowStore } from "@/stores/workflow";
+import { getErrMsg } from "@/utils/error";
+
+const WorkflowDetailDesign = () => {
+ const { t } = useTranslation();
+
+ const { message, modal, notification } = App.useApp();
+
+ const { workflow, ...workflowState } = useWorkflowStore(useZustandShallowSelector(["workflow", "publish", "rollback"]));
+
+ const [isPendingOrRunning, setIsPendingOrRunning] = useState(false);
+ const lastRunStatus = useMemo(() => workflow.lastRunStatus, [workflow]);
+
+ const [allowRollback, setAllowRollback] = useState(false);
+ const [allowPublish, setAllowPublish] = useState(false);
+ const [allowRun, setAllowRun] = useState(false);
+
+ useEffect(() => {
+ setIsPendingOrRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.PENDING || lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING);
+ }, [lastRunStatus]);
+
+ useEffect(() => {
+ if (isPendingOrRunning) {
+ subscribeWorkflow(workflow.id, (cb) => {
+ if (cb.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.PENDING && cb.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) {
+ setIsPendingOrRunning(false);
+ unsubscribeWorkflow(workflow.id);
+ }
+ });
+
+ return () => {
+ unsubscribeWorkflow(workflow.id);
+ };
+ }
+ }, [workflow.id, isPendingOrRunning]);
+
+ useEffect(() => {
+ const hasContent = !!workflow.content;
+ const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content);
+ setAllowRollback(!isPendingOrRunning && hasContent && hasChanges);
+ setAllowPublish(!isPendingOrRunning && hasChanges);
+ setAllowRun(hasContent);
+ }, [workflow.content, workflow.draft, workflow.hasDraft, isPendingOrRunning]);
+
+ const handleRollbackClick = () => {
+ modal.confirm({
+ title: t("workflow.action.rollback.modal.title"),
+ content: t("workflow.action.rollback.modal.content"),
+ onOk: async () => {
+ try {
+ await workflowState.rollback();
+
+ message.success(t("common.text.operation_succeeded"));
+ } catch (err) {
+ console.error(err);
+ notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
+ }
+ },
+ });
+ };
+
+ const handlePublishClick = () => {
+ if (!isAllNodesValidated(workflow.draft!)) {
+ message.warning(t("workflow.action.publish.errmsg.uncompleted"));
+ return;
+ }
+
+ modal.confirm({
+ title: t("workflow.action.publish.modal.title"),
+ content: t("workflow.action.publish.modal.content"),
+ onOk: async () => {
+ try {
+ await workflowState.publish();
+
+ message.success(t("common.text.operation_succeeded"));
+ } catch (err) {
+ console.error(err);
+ notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
+ }
+ },
+ });
+ };
+
+ const handleRunClick = () => {
+ const { promise, resolve, reject } = Promise.withResolvers();
+ if (workflow.hasDraft) {
+ modal.confirm({
+ title: t("workflow.action.run.modal.title"),
+ content: t("workflow.action.run.modal.content"),
+ onOk: () => resolve(void 0),
+ onCancel: () => reject(),
+ });
+ } else {
+ resolve(void 0);
+ }
+
+ promise.then(async () => {
+ let unsubscribeFn: Awaited> | undefined = undefined;
+
+ try {
+ setIsPendingOrRunning(true);
+
+ // subscribe before running workflow
+ unsubscribeFn = await subscribeWorkflow(workflow.id, (e) => {
+ if (e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.PENDING && e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) {
+ setIsPendingOrRunning(false);
+ unsubscribeFn?.();
+ }
+ });
+
+ await startWorkflowRun(workflow.id);
+
+ message.info(t("workflow.action.run.prompt"));
+ } catch (err) {
+ setIsPendingOrRunning(false);
+ unsubscribeFn?.();
+
+ console.error(err);
+ message.warning(t("common.text.operation_failed"));
+ }
+ });
+ };
+
+ return (
+
+
+
+
+
+
+ {t("workflow.detail.design.draft.alert")}} showIcon type="warning" />
+
+
+
+
+ } loading={isPendingOrRunning} type="primary" onClick={handleRunClick}>
+ {t("workflow.action.run.button")}
+
+
+
+ {t("workflow.action.publish.button")}
+
+ ,
+ onClick: handleRollbackClick,
+ },
+ ],
+ }}
+ trigger={["click"]}
+ >
+ } variant="outlined" />
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default WorkflowDetailDesign;
diff --git a/ui/src/components/workflow/WorkflowRuns.tsx b/ui/src/pages/workflows/WorkflowDetailRuns.tsx
similarity index 80%
rename from ui/src/components/workflow/WorkflowRuns.tsx
rename to ui/src/pages/workflows/WorkflowDetailRuns.tsx
index 72892bc57..9f597bab2 100644
--- a/ui/src/components/workflow/WorkflowRuns.tsx
+++ b/ui/src/pages/workflows/WorkflowDetailRuns.tsx
@@ -8,32 +8,29 @@ import { ClientResponseError } from "pocketbase";
import { cancelRun as cancelWorkflowRun } from "@/api/workflows";
import Empty from "@/components/Empty";
+import WorkflowRunDetailDrawer from "@/components/workflow/WorkflowRunDetailDrawer";
import WorkflowStatusTag from "@/components/workflow/WorkflowStatusTag";
import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
-import { useAppSettings } from "@/hooks";
+import { useAppSettings, useZustandShallowSelector } from "@/hooks";
import {
list as listWorkflowRuns,
remove as removeWorkflowRun,
subscribe as subscribeWorkflowRun,
unsubscribe as unsubscribeWorkflowRun,
} from "@/repository/workflowRun";
+import { useWorkflowStore } from "@/stores/workflow";
import { getErrMsg } from "@/utils/error";
-import WorkflowRunDetailDrawer from "./WorkflowRunDetailDrawer";
-export interface WorkflowRunsProps {
- className?: string;
- style?: React.CSSProperties;
- workflowId: string;
-}
-
-const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
+const WorkflowDetailRuns = () => {
const { t } = useTranslation();
const { modal, notification } = App.useApp();
const { appSettings: globalAppSettings } = useAppSettings();
+ const { workflow } = useWorkflowStore(useZustandShallowSelector(["workflow"]));
+
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(globalAppSettings.defaultPerPage!);
@@ -167,13 +164,13 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
} = useRequest(
() => {
return listWorkflowRuns({
- workflowId: workflowId,
+ workflowId: workflow.id,
page: page,
perPage: pageSize,
});
},
{
- refreshDeps: [workflowId, page, pageSize],
+ refreshDeps: [workflow.id, page, pageSize],
onSuccess: (res) => {
setTableData(res.items);
setTableTotal(res.totalItems);
@@ -238,7 +235,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
content: t("workflow_run.action.cancel.modal.content"),
onOk: async () => {
try {
- const resp = await cancelWorkflowRun(workflowId, workflowRun.id);
+ const resp = await cancelWorkflowRun(workflow.id, workflowRun.id);
if (resp) {
refreshData();
}
@@ -277,46 +274,48 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
};
return (
-
-
} showIcon type="info" />
- } showIcon type="info" />
+
+
+
} showIcon type="info" />
+ } showIcon type="info" />
-
- columns={tableColumns}
- dataSource={tableData}
- loading={loading}
- locale={{
- emptyText: loading ? (
-
- ) : (
- }
- />
- ),
- }}
- pagination={{
- current: page,
- pageSize: pageSize,
- total: tableTotal,
- showSizeChanger: true,
- onChange: handlePaginationChange,
- onShowSizeChange: handlePaginationChange,
- }}
- rowClassName="cursor-pointer"
- rowKey={(record) => record.id}
- scroll={{ x: "max(100%, 960px)" }}
- onRow={(record) => ({
- onClick: () => {
- handleRecordDetailClick(record);
- },
- })}
- />
+
+ columns={tableColumns}
+ dataSource={tableData}
+ loading={loading}
+ locale={{
+ emptyText: loading ? (
+
+ ) : (
+ }
+ />
+ ),
+ }}
+ pagination={{
+ current: page,
+ pageSize: pageSize,
+ total: tableTotal,
+ showSizeChanger: true,
+ onChange: handlePaginationChange,
+ onShowSizeChange: handlePaginationChange,
+ }}
+ rowClassName="cursor-pointer"
+ rowKey={(record) => record.id}
+ scroll={{ x: "max(100%, 960px)" }}
+ onRow={(record) => ({
+ onClick: () => {
+ handleRecordDetailClick(record);
+ },
+ })}
+ />
-
+
+
);
};
-export default WorkflowRuns;
+export default WorkflowDetailRuns;
diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx
index 9f9e93f46..97373a875 100644
--- a/ui/src/pages/workflows/WorkflowList.tsx
+++ b/ui/src/pages/workflows/WorkflowList.tsx
@@ -79,22 +79,22 @@ const WorkflowList = () => {
title: t("workflow.props.state"),
defaultFilteredValue: searchParams.has("state") ? [searchParams.get("state") as string] : undefined,
render: (_, record) => {
- const enabled = record.enabled;
return (
- {
- e.stopPropagation();
+ {
+ handleRecordActiveChange(record);
}}
- >
- {
- handleRecordActiveChange(record);
- }}
- />
-
+ />
);
},
+ onCell: () => {
+ return {
+ onClick: (e) => {
+ e.stopPropagation();
+ },
+ };
+ },
},
{
key: "lastRun",
@@ -102,12 +102,17 @@ const WorkflowList = () => {
sorter: true,
sortOrder: sorter.columnKey === "lastRun" ? sorter.order : undefined,
render: (_, record) => {
- return (
-
-
- {record.lastRunTime ? dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss") : ""}
-
- );
+ const { lastRunStatus, lastRunTime } = record;
+ if (!lastRunStatus) {
+ return <>>;
+ } else {
+ return (
+
+
+ {lastRunTime ? dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss") : ""}
+
+ );
+ }
},
},
{
diff --git a/ui/src/pages/workflows/WorkflowNew.tsx b/ui/src/pages/workflows/WorkflowNew.tsx
index 7a1583e27..6524fb1f3 100644
--- a/ui/src/pages/workflows/WorkflowNew.tsx
+++ b/ui/src/pages/workflows/WorkflowNew.tsx
@@ -1,18 +1,15 @@
-import { useEffect, useRef, useState } from "react";
+import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
-import { App, Card, Col, Form, Input, type InputRef, Row, Spin, Typography } from "antd";
-import { createSchemaFieldRule } from "antd-zod";
-import { z } from "zod";
+import { App, Card, Col, Row, Spin, Typography } from "antd";
+import dayjs from "dayjs";
-import ModalForm from "@/components/ModalForm";
import { type WorkflowModel, initWorkflow } from "@/domain/workflow";
-import { useAntdForm } from "@/hooks";
import { save as saveWorkflow } from "@/repository/workflow";
import { getErrMsg } from "@/utils/error";
const TEMPLATE_KEY_STANDARD = "standard" as const;
-const TEMPLATE_KEY_CERTTEST = "monitor" as const;
+const TEMPLATE_KEY_CERTTEST = "certtest" as const;
const TEMPLATE_KEY_EMPTY = "empty" as const;
type TemplateKeys = typeof TEMPLATE_KEY_EMPTY | typeof TEMPLATE_KEY_CERTTEST | typeof TEMPLATE_KEY_STANDARD;
@@ -32,83 +29,45 @@ const WorkflowNew = () => {
};
const [templateSelectKey, setTemplateSelectKey] = useState();
- const formSchema = z.object({
- name: z
- .string()
- .min(1, t("workflow.new.modal.form.name.placeholder"))
- .max(64, t("common.errmsg.string_max", { max: 64 })),
- description: z
- .string()
- .max(256, t("common.errmsg.string_max", { max: 256 }))
- .nullish(),
- });
- const formRule = createSchemaFieldRule(formSchema);
- const {
- form: formInst,
- formPending,
- formProps,
- submit: submitForm,
- } = useAntdForm>({
- initialValues: {
- name: "",
- description: "",
- },
- onSubmit: async (values) => {
- try {
- let workflow: WorkflowModel;
+ const [pending, setPending] = useState(false);
- switch (templateSelectKey) {
- case TEMPLATE_KEY_EMPTY:
- workflow = initWorkflow();
- break;
+ const handleTemplateClick = async (key: TemplateKeys) => {
+ if (pending) return;
- case TEMPLATE_KEY_STANDARD:
- workflow = initWorkflow({ template: "standard" });
- break;
-
- case TEMPLATE_KEY_CERTTEST:
- workflow = initWorkflow({ template: "certtest" });
- break;
-
- default:
- throw "Invalid state: `templateSelectKey`";
- }
-
- workflow.name = values.name?.trim() ?? workflow.name;
- workflow.description = values.description?.trim() ?? workflow.description;
- workflow = await saveWorkflow(workflow);
- navigate(`/workflows/${workflow.id}`, { replace: true });
- } catch (err) {
- notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
-
- throw err;
- }
- },
- });
- const [formModalOpen, setFormModalOpen] = useState(false);
-
- useEffect(() => {
- if (formModalOpen) {
- setTimeout(() => inputRef.current?.focus({ cursor: "end" }), 1);
- } else {
- setTemplateSelectKey(undefined);
- formInst.resetFields();
- }
- }, [formModalOpen]);
-
- const inputRef = useRef(null);
-
- const handleTemplateClick = (key: TemplateKeys) => {
setTemplateSelectKey(key);
- setFormModalOpen(true);
- };
- const handleModalOpenChange = (open: boolean) => {
- setFormModalOpen(open);
- };
+ try {
+ let workflow: WorkflowModel;
- const handleModalFormFinish = () => {
- return submitForm();
+ switch (key) {
+ case TEMPLATE_KEY_EMPTY:
+ workflow = initWorkflow();
+ break;
+
+ case TEMPLATE_KEY_STANDARD:
+ workflow = initWorkflow({ template: "standard" });
+ break;
+
+ case TEMPLATE_KEY_CERTTEST:
+ workflow = initWorkflow({ template: "certtest" });
+ break;
+
+ default:
+ throw "Invalid state: `templateSelectKey`";
+ }
+
+ workflow.name = t("workflow.new.templates.default_name");
+ workflow.description = t("workflow.new.templates.default_description", { date: dayjs().format("YYYY-MM-DD HH:mm") });
+ workflow = await saveWorkflow(workflow);
+ navigate(`/workflows/${workflow.id}`, { replace: true });
+ } catch (err) {
+ notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
+
+ throw err;
+ } finally {
+ setPending(false);
+ setTemplateSelectKey(undefined);
+ }
};
return (
@@ -179,29 +138,6 @@ const WorkflowNew = () => {
-
-
-
-
-
-
-
-
-
-
);
};
diff --git a/ui/src/routers/index.tsx b/ui/src/routers/index.tsx
index 78ff91261..5e192a1e7 100644
--- a/ui/src/routers/index.tsx
+++ b/ui/src/routers/index.tsx
@@ -14,6 +14,8 @@ import SettingsAppearance from "@/pages/settings/SettingsAppearance";
import SettingsPersistence from "@/pages/settings/SettingsPersistence";
import SettingsSSLProvider from "@/pages/settings/SettingsSSLProvider";
import WorkflowDetail from "@/pages/workflows/WorkflowDetail";
+import WorkflowDetailDesign from "@/pages/workflows/WorkflowDetailDesign";
+import WorkflowDetailRuns from "@/pages/workflows/WorkflowDetailRuns";
import WorkflowList from "@/pages/workflows/WorkflowList";
import WorkflowNew from "@/pages/workflows/WorkflowNew";
@@ -49,6 +51,16 @@ export const router = createHashRouter([
{
path: "/workflows/:id",
element: ,
+ children: [
+ {
+ path: "/workflows/:id/design",
+ element: ,
+ },
+ {
+ path: "/workflows/:id/runs",
+ element: ,
+ },
+ ],
},
{
path: "/settings",
diff --git a/ui/src/stores/workflow/index.ts b/ui/src/stores/workflow/index.ts
index 7a0867083..405a5bb20 100644
--- a/ui/src/stores/workflow/index.ts
+++ b/ui/src/stores/workflow/index.ts
@@ -23,8 +23,8 @@ export type WorkflowState = {
init(id: string): void;
setBaseInfo: (name: string, description: string) => void;
setEnabled(enabled: boolean): void;
- release(): void;
- discard(): void;
+ publish(): void;
+ rollback(): void;
destroy(): void;
addNode: (node: WorkflowNode, previousNodeId: string) => void;
@@ -95,7 +95,7 @@ export const useWorkflowStore = create((set, get) => ({
});
},
- release: async () => {
+ publish: async () => {
if (!get().initialized) throw "Workflow not initialized yet";
const root = get().workflow.draft!;
@@ -121,7 +121,7 @@ export const useWorkflowStore = create((set, get) => ({
});
},
- discard: async () => {
+ rollback: async () => {
if (!get().initialized) throw "Workflow not initialized yet";
const root = get().workflow.content!;