mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-24 23:10:13 +08:00
feat(ui): new workflow routers
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "执行历史"
|
||||
}
|
||||
|
||||
@@ -16,17 +16,22 @@ const Settings = () => {
|
||||
["ssl-provider", "settings.sslprovider.tab", <IconPlugConnected size="1em" />],
|
||||
["persistence", "settings.persistence.tab", <IconDatabaseCog size="1em" />],
|
||||
] 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 (
|
||||
<div className="px-6 py-4">
|
||||
<div className="mx-auto max-w-320">
|
||||
@@ -46,10 +51,7 @@ const Settings = () => {
|
||||
</span>
|
||||
),
|
||||
}))}
|
||||
onClick={({ key }) => {
|
||||
setMenuValue(key);
|
||||
navigate(`/settings/${key}`);
|
||||
}}
|
||||
onClick={handleMenuClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -68,10 +70,7 @@ const Settings = () => {
|
||||
</span>
|
||||
),
|
||||
}))}
|
||||
onClick={({ key }) => {
|
||||
setMenuValue(key);
|
||||
navigate(`/settings/${key}`);
|
||||
}}
|
||||
onClick={handleMenuClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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", <IconRobot size="1em" />],
|
||||
["runs", "workflow.detail.runs.tab", <IconHistory size="1em" />],
|
||||
] 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<ReturnType<typeof subscribeWorkflow>> | 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 (
|
||||
<div className="flex size-full flex-col">
|
||||
<div className="px-6 py-4">
|
||||
@@ -205,7 +104,7 @@ const WorkflowDetail = () => {
|
||||
{initialized
|
||||
? [
|
||||
<WorkflowBaseInfoModal key="edit" trigger={<Button>{t("common.button.edit")}</Button>} />,
|
||||
<Button key="enable" onClick={handleEnableChange}>
|
||||
<Button key="enable" onClick={handleEnableClick}>
|
||||
{workflow.enabled ? t("workflow.action.disable.button") : t("workflow.action.enable.button")}
|
||||
</Button>,
|
||||
<Dropdown
|
||||
@@ -214,7 +113,7 @@ const WorkflowDetail = () => {
|
||||
items: [
|
||||
{
|
||||
key: "delete",
|
||||
label: t("common.button.delete"),
|
||||
label: t("workflow.action.delete.button"),
|
||||
danger: true,
|
||||
icon: <IconTrash size="1.25em" />,
|
||||
onClick: () => {
|
||||
@@ -237,98 +136,36 @@ const WorkflowDetail = () => {
|
||||
<div className="absolute -bottom-12 left-1/2 z-1 -translate-x-1/2">
|
||||
<Segmented
|
||||
className="shadow"
|
||||
options={[
|
||||
{
|
||||
value: "orchestration",
|
||||
label: <span className="px-2 text-sm">{t("workflow.detail.orchestration.tab")}</span>,
|
||||
icon: (
|
||||
<span className="anticon scale-125" role="img">
|
||||
<IconRobot size="1em" />
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "runs",
|
||||
label: <span className="px-2 text-sm">{t("workflow.detail.runs.tab")}</span>,
|
||||
icon: (
|
||||
<span className="anticon scale-125" role="img">
|
||||
<IconHistory size="1em" />
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
options={tabs.map(([key, label, icon]) => ({
|
||||
value: key,
|
||||
label: <span className="px-2 text-sm">{t(label)}</span>,
|
||||
icon: (
|
||||
<span className="anticon scale-125" role="img">
|
||||
{icon}
|
||||
</span>
|
||||
),
|
||||
}))}
|
||||
size="large"
|
||||
value={tabValue}
|
||||
defaultValue="orchestration"
|
||||
onChange={(value) => {
|
||||
setTabValue(value as typeof tabValue);
|
||||
}}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={tabValue === "orchestration"}>
|
||||
<div className="min-h-[360px] flex-1 overflow-hidden p-4">
|
||||
<Card
|
||||
className="size-full overflow-hidden"
|
||||
styles={{
|
||||
body: {
|
||||
position: "relative",
|
||||
height: "100%",
|
||||
padding: initialized ? 0 : undefined,
|
||||
},
|
||||
}}
|
||||
loading={!initialized}
|
||||
>
|
||||
<div className="absolute inset-x-6 top-4 z-2 mx-auto flex max-w-320 items-center justify-between gap-4 pt-6">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Show when={workflow.hasDraft!}>
|
||||
<Alert message={<div className="truncate">{t("workflow.detail.orchestration.draft.alert")}</div>} showIcon type="warning" />
|
||||
</Show>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Space>
|
||||
<Button disabled={!allowRun} icon={<IconPlayerPlay size="1.25em" />} loading={isPendingOrRunning} type="primary" onClick={handleRunClick}>
|
||||
{t("workflow.action.run.button")}
|
||||
</Button>
|
||||
<Space.Compact>
|
||||
<Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}>
|
||||
{t("workflow.action.release.button")}
|
||||
</Button>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "discard",
|
||||
disabled: !allowDiscard,
|
||||
label: t("workflow.action.discard.button"),
|
||||
icon: <IconArrowBackUp size="1.25em" />,
|
||||
onClick: handleDiscardClick,
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button color="primary" disabled={!allowDiscard} icon={<IconDots size="1.25em" />} variant="outlined" />
|
||||
</Dropdown>
|
||||
</Space.Compact>
|
||||
</Space>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<Show
|
||||
when={initialized}
|
||||
fallback={
|
||||
<div className="mx-auto max-w-320 pt-12">
|
||||
<Skeleton active />
|
||||
</div>
|
||||
|
||||
<WorkflowElementsContainer className="pt-24" />
|
||||
</Card>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={tabValue === "runs"}>
|
||||
<div className="p-4">
|
||||
<div className="mx-auto max-w-320 pt-6">
|
||||
<WorkflowRuns workflowId={workflowId!} />
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<ReturnType<typeof subscribeWorkflow>> | 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 (
|
||||
<div className="min-h-[360px] flex-1 overflow-hidden">
|
||||
<Card
|
||||
className="size-full overflow-hidden"
|
||||
styles={{
|
||||
body: {
|
||||
position: "relative",
|
||||
height: "100%",
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="pt-9">
|
||||
<div className="absolute inset-x-6 z-2 mx-auto flex max-w-320 items-center justify-between gap-4">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Show when={workflow.hasDraft!}>
|
||||
<Alert message={<div className="truncate">{t("workflow.detail.design.draft.alert")}</div>} showIcon type="warning" />
|
||||
</Show>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Space>
|
||||
<Button disabled={!allowRun} icon={<IconPlayerPlay size="1.25em" />} loading={isPendingOrRunning} type="primary" onClick={handleRunClick}>
|
||||
{t("workflow.action.run.button")}
|
||||
</Button>
|
||||
<Space.Compact>
|
||||
<Button color="primary" disabled={!allowPublish} variant="outlined" onClick={handlePublishClick}>
|
||||
{t("workflow.action.publish.button")}
|
||||
</Button>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "rollback",
|
||||
disabled: !allowRollback,
|
||||
label: t("workflow.action.rollback.button"),
|
||||
icon: <IconArrowBackUp size="1.25em" />,
|
||||
onClick: handleRollbackClick,
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button color="primary" icon={<IconDots size="1.25em" />} variant="outlined" />
|
||||
</Dropdown>
|
||||
</Space.Compact>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WorkflowElementsContainer className="pt-12" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowDetailDesign;
|
||||
+48
-49
@@ -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<number>(1);
|
||||
const [pageSize, setPageSize] = useState<number>(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 (
|
||||
<div className={className} style={style}>
|
||||
<Alert className="mb-4" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_run.deletion.alert") }}></span>} showIcon type="info" />
|
||||
<Alert className="mb-4" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_run.cancellation.alert") }}></span>} showIcon type="info" />
|
||||
<div className="mx-auto max-w-320">
|
||||
<div className="pt-9">
|
||||
<Alert className="mb-4" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_run.deletion.alert") }}></span>} showIcon type="info" />
|
||||
<Alert className="mb-4" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_run.cancellation.alert") }}></span>} showIcon type="info" />
|
||||
|
||||
<Table<WorkflowRunModel>
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: loading ? (
|
||||
<Skeleton />
|
||||
) : (
|
||||
<Empty
|
||||
title={t("common.text.nodata")}
|
||||
description={loadedError ? getErrMsg(loadedError) : t("workflow_run.nodata.description")}
|
||||
icon={<IconHistory size={24} />}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
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);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<Table<WorkflowRunModel>
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: loading ? (
|
||||
<Skeleton />
|
||||
) : (
|
||||
<Empty
|
||||
title={t("common.text.nodata")}
|
||||
description={loadedError ? getErrMsg(loadedError) : t("workflow_run.nodata.description")}
|
||||
icon={<IconHistory size={24} />}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
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);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
<WorkflowRunDetailDrawer {...detailDrawerProps} />
|
||||
<WorkflowRunDetailDrawer {...detailDrawerProps} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowRuns;
|
||||
export default WorkflowDetailRuns;
|
||||
@@ -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 (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
<Switch
|
||||
checked={record.enabled}
|
||||
onChange={() => {
|
||||
handleRecordActiveChange(record);
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
handleRecordActiveChange(record);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
);
|
||||
},
|
||||
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 (
|
||||
<Flex gap="small">
|
||||
<WorkflowStatusIcon color={true} size="1.25em" status={record.lastRunStatus!} />
|
||||
<Typography.Text>{record.lastRunTime ? dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss") : ""}</Typography.Text>
|
||||
</Flex>
|
||||
);
|
||||
const { lastRunStatus, lastRunTime } = record;
|
||||
if (!lastRunStatus) {
|
||||
return <></>;
|
||||
} else {
|
||||
return (
|
||||
<Flex gap="small">
|
||||
<WorkflowStatusIcon color={true} size="1.25em" status={lastRunStatus!} />
|
||||
<Typography.Text>{lastRunTime ? dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss") : ""}</Typography.Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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<TemplateKeys>();
|
||||
|
||||
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<z.infer<typeof formSchema>>({
|
||||
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<InputRef>(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 = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<ModalForm
|
||||
{...formProps}
|
||||
autoFocus
|
||||
disabled={formPending}
|
||||
layout="vertical"
|
||||
form={formInst}
|
||||
modalProps={{ destroyOnHidden: true }}
|
||||
okText={t("common.button.submit")}
|
||||
open={formModalOpen}
|
||||
title={t(`workflow.new.modal.title`)}
|
||||
width={480}
|
||||
onFinish={handleModalFormFinish}
|
||||
onOpenChange={handleModalOpenChange}
|
||||
>
|
||||
<Form.Item name="name" label={t("workflow.new.modal.form.name.label")} rules={[formRule]}>
|
||||
<Input ref={inputRef} autoFocus placeholder={t("workflow.new.modal.form.name.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label={t("workflow.new.modal.form.description.label")} rules={[formRule]}>
|
||||
<Input placeholder={t("workflow.new.modal.form.description.placeholder")} />
|
||||
</Form.Item>
|
||||
</ModalForm>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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: <WorkflowDetail />,
|
||||
children: [
|
||||
{
|
||||
path: "/workflows/:id/design",
|
||||
element: <WorkflowDetailDesign />,
|
||||
},
|
||||
{
|
||||
path: "/workflows/:id/runs",
|
||||
element: <WorkflowDetailRuns />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
|
||||
@@ -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<WorkflowState>((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<WorkflowState>((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
discard: async () => {
|
||||
rollback: async () => {
|
||||
if (!get().initialized) throw "Workflow not initialized yet";
|
||||
|
||||
const root = get().workflow.content!;
|
||||
|
||||
Reference in New Issue
Block a user