From bc268dfb325af7211773b3a00559e453e3d5644c Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Fri, 25 Jul 2025 19:10:26 +0800 Subject: [PATCH] feat(ui): new workflow routers --- ui/src/i18n/locales/en/nls.workflow.json | 27 +- ui/src/i18n/locales/zh/nls.workflow.json | 25 +- ui/src/pages/settings/Settings.tsx | 23 +- ui/src/pages/workflows/WorkflowDetail.tsx | 257 ++++-------------- .../pages/workflows/WorkflowDetailDesign.tsx | 193 +++++++++++++ .../workflows/WorkflowDetailRuns.tsx} | 97 ++++--- ui/src/pages/workflows/WorkflowList.tsx | 41 +-- ui/src/pages/workflows/WorkflowNew.tsx | 140 +++------- ui/src/routers/index.tsx | 12 + ui/src/stores/workflow/index.ts | 8 +- 10 files changed, 399 insertions(+), 424 deletions(-) create mode 100644 ui/src/pages/workflows/WorkflowDetailDesign.tsx rename ui/src/{components/workflow/WorkflowRuns.tsx => pages/workflows/WorkflowDetailRuns.tsx} (80%) 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")}} />, - , { items: [ { key: "delete", - label: t("common.button.delete"), + label: t("workflow.action.delete.button"), danger: true, icon: , onClick: () => { @@ -237,98 +136,36 @@ const WorkflowDetail = () => {
{t("workflow.detail.orchestration.tab")}, - icon: ( - - - - ), - }, - { - value: "runs", - label: {t("workflow.detail.runs.tab")}, - icon: ( - - - - ), - }, - ]} + options={tabs.map(([key, label, icon]) => ({ + value: key, + label: {t(label)}, + icon: ( + + {icon} + + ), + }))} size="large" value={tabValue} defaultValue="orchestration" - onChange={(value) => { - setTabValue(value as typeof tabValue); - }} + onChange={handleTabChange} />
- -
- -
-
- - {t("workflow.detail.orchestration.draft.alert")}
} showIcon type="warning" /> - -
-
- - - - - , - onClick: handleDiscardClick, - }, - ], - }} - trigger={["click"]} - > -
+
+ +
- - -
-
-
- - -
-
- -
-
-
+ } + > + + + ); }; 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" /> + +
+
+ + + + + , + onClick: handleRollbackClick, + }, + ], + }} + trigger={["click"]} + > +
+
+ + +
+ + + ); +}; + +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!;