import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useParams } from "react-router-dom"; import { ApartmentOutlined as ApartmentOutlinedIcon, CaretRightOutlined as CaretRightOutlinedIcon, DeleteOutlined as DeleteOutlinedIcon, DownOutlined as DownOutlinedIcon, EllipsisOutlined as EllipsisOutlinedIcon, HistoryOutlined as HistoryOutlinedIcon, UndoOutlined as UndoOutlinedIcon, } from "@ant-design/icons"; import { PageHeader } from "@ant-design/pro-components"; import { Alert, Button, Card, Dropdown, Form, Input, Modal, Space, Tabs, Typography, message, notification } 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 { useWorkflowStore } from "@/stores/workflow"; import { getErrMsg } from "@/utils/error"; const WorkflowDetail = () => { const navigate = useNavigate(); const { t } = useTranslation(); const [messageApi, MessageContextHolder] = message.useMessage(); const [modalApi, ModalContextHolder] = Modal.useModal(); const [notificationApi, NotificationContextHolder] = notification.useNotification(); const { id: workflowId } = useParams(); const { workflow, initialized, ...workflowState } = useWorkflowStore( useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled", "release", "discard"]) ); useEffect(() => { workflowState.init(workflowId!); return () => { workflowState.destroy(); }; }, [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); useEffect(() => { setIsPendingOrRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.PENDING || lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING); }, [lastRunStatus]); useEffect(() => { if (!!workflowId && isPendingOrRunning) { subscribeWorkflow(workflowId, (e) => { if (e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.PENDING && e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) { setIsPendingOrRunning(false); unsubscribeWorkflow(workflowId); } }); return () => { unsubscribeWorkflow(workflowId); }; } }, [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]); const handleEnableChange = async () => { if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) { messageApi.warning(t("workflow.action.enable.failed.uncompleted")); return; } try { await workflowState.setEnabled(!workflow.enabled); } catch (err) { console.error(err); notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); } }; const handleDeleteClick = () => { modalApi.confirm({ title: t("workflow.action.delete"), content: t("workflow.action.delete.confirm"), onOk: async () => { try { const resp = await removeWorkflow(workflow); if (resp) { navigate("/workflows", { replace: true }); } } catch (err) { console.error(err); notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); } }, }); }; const handleDiscardClick = () => { modalApi.confirm({ title: t("workflow.detail.orchestration.action.discard"), content: t("workflow.detail.orchestration.action.discard.confirm"), onOk: async () => { try { await workflowState.discard(); messageApi.success(t("common.text.operation_succeeded")); } catch (err) { console.error(err); notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); } }, }); }; const handleReleaseClick = () => { if (!isAllNodesValidated(workflow.draft!)) { messageApi.warning(t("workflow.detail.orchestration.action.release.failed.uncompleted")); return; } modalApi.confirm({ title: t("workflow.detail.orchestration.action.release"), content: t("workflow.detail.orchestration.action.release.confirm"), onOk: async () => { try { await workflowState.release(); messageApi.success(t("common.text.operation_succeeded")); } catch (err) { console.error(err); notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); } }, }); }; const handleRunClick = () => { const { promise, resolve, reject } = Promise.withResolvers(); if (workflow.hasDraft) { modalApi.confirm({ title: t("workflow.detail.orchestration.action.run"), content: t("workflow.detail.orchestration.action.run.confirm"), 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!); messageApi.info(t("workflow.detail.orchestration.action.run.prompt")); } catch (err) { setIsPendingOrRunning(false); unsubscribeFn?.(); console.error(err); messageApi.warning(t("common.text.operation_failed")); } }); }; return (
{MessageContextHolder} {ModalContextHolder} {NotificationContextHolder}
{t("common.button.edit")}} />, , , onClick: () => { handleDeleteClick(); }, }, ], }} trigger={["click"]} > , ] : [] } > {workflow.description} }, { key: "runs", label: t("workflow.detail.runs.tab"), icon: }, ]} renderTabBar={(props, DefaultTabBar) => } tabBarStyle={{ border: "none" }} onChange={(key) => setTabValue(key as typeof tabValue)} />
{t("workflow.detail.orchestration.draft.alert")}
} type="warning" />
, onClick: handleDiscardClick, }, ], }} trigger={["click"]} >
); }; const WorkflowBaseInfoModal = ({ trigger }: { trigger?: React.ReactNode }) => { const { t } = useTranslation(); const [notificationApi, NotificationContextHolder] = notification.useNotification(); const { workflow, ...workflowState } = useWorkflowStore(useZustandShallowSelector(["workflow", "setBaseInfo"])); const formSchema = z.object({ name: z .string({ message: t("workflow.detail.baseinfo.form.name.placeholder") }) .min(1, t("workflow.detail.baseinfo.form.name.placeholder")) .max(64, t("common.errmsg.string_max", { max: 64 })) .trim(), description: z .string({ message: t("workflow.detail.baseinfo.form.description.placeholder") }) .max(256, t("common.errmsg.string_max", { max: 256 })) .trim() .nullish(), }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formPending, formProps, submit: submitForm, } = useAntdForm>({ initialValues: { name: workflow.name, description: workflow.description }, onSubmit: async (values) => { try { await workflowState.setBaseInfo(values.name!, values.description!); } catch (err) { notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); throw err; } }, }); const handleFormFinish = async () => { return submitForm(); }; return ( <> {NotificationContextHolder} ); }; export default WorkflowDetail;