mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-25 22:00:07 +00:00
359 lines
12 KiB
TypeScript
359 lines
12 KiB
TypeScript
import { useEffect, 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 { ClientResponseError } from "pocketbase";
|
|
import { isEqual } from "radash";
|
|
import { z } from "zod";
|
|
|
|
import { run as runWorkflow } from "@/api/workflow";
|
|
import ModalForm from "@/components/ModalForm";
|
|
import Show from "@/components/Show";
|
|
import WorkflowElements from "@/components/workflow/WorkflowElements";
|
|
import WorkflowRuns from "@/components/workflow/WorkflowRuns";
|
|
import { isAllNodesValidated } from "@/domain/workflow";
|
|
import { useAntdForm, useZustandShallowSelector } from "@/hooks";
|
|
import { remove as removeWorkflow } 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(() => {
|
|
// TODO: loading & error
|
|
workflowState.init(workflowId!);
|
|
|
|
return () => {
|
|
workflowState.destroy();
|
|
};
|
|
}, [workflowId]);
|
|
|
|
const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration");
|
|
|
|
const [isRunning, setIsRunning] = useState(false);
|
|
|
|
const [allowDiscard, setAllowDiscard] = useState(false);
|
|
const [allowRelease, setAllowRelease] = useState(false);
|
|
const [allowRun, setAllowRun] = useState(false);
|
|
useEffect(() => {
|
|
const hasReleased = !!workflow.content;
|
|
const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content);
|
|
setAllowDiscard(!isRunning && hasReleased && hasChanges);
|
|
setAllowRelease(!isRunning && hasChanges);
|
|
setAllowRun(hasReleased);
|
|
}, [workflow.content, workflow.draft, workflow.hasDraft, isRunning]);
|
|
|
|
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: boolean = 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);
|
|
}
|
|
|
|
// TODO: 异步执行
|
|
promise.then(async () => {
|
|
setIsRunning(true);
|
|
|
|
try {
|
|
await runWorkflow(workflowId!);
|
|
|
|
messageApi.success(t("common.text.operation_succeeded"));
|
|
} catch (err) {
|
|
if (err instanceof ClientResponseError && err.isAbort) {
|
|
return;
|
|
}
|
|
|
|
console.error(err);
|
|
messageApi.warning(t("common.text.operation_failed"));
|
|
} finally {
|
|
setIsRunning(false);
|
|
}
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
{MessageContextHolder}
|
|
{ModalContextHolder}
|
|
{NotificationContextHolder}
|
|
|
|
<Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }}>
|
|
<PageHeader
|
|
style={{ paddingBottom: 0 }}
|
|
title={workflow.name}
|
|
extra={
|
|
initialized
|
|
? [
|
|
<WorkflowBaseInfoModal key="edit" trigger={<Button>{t("common.button.edit")}</Button>} />,
|
|
|
|
<Button key="enable" onClick={handleEnableChange}>
|
|
{workflow.enabled ? t("workflow.action.disable") : t("workflow.action.enable")}
|
|
</Button>,
|
|
|
|
<Dropdown
|
|
key="more"
|
|
menu={{
|
|
items: [
|
|
{
|
|
key: "delete",
|
|
label: t("workflow.action.delete"),
|
|
danger: true,
|
|
icon: <DeleteOutlinedIcon />,
|
|
onClick: () => {
|
|
handleDeleteClick();
|
|
},
|
|
},
|
|
],
|
|
}}
|
|
trigger={["click"]}
|
|
>
|
|
<Button icon={<DownOutlinedIcon />} iconPosition="end">
|
|
{t("common.button.more")}
|
|
</Button>
|
|
</Dropdown>,
|
|
]
|
|
: []
|
|
}
|
|
>
|
|
<Typography.Paragraph type="secondary">{workflow.description}</Typography.Paragraph>
|
|
<Tabs
|
|
activeKey={tabValue}
|
|
defaultActiveKey="orchestration"
|
|
items={[
|
|
{ key: "orchestration", label: t("workflow.detail.orchestration.tab"), icon: <ApartmentOutlinedIcon /> },
|
|
{ key: "runs", label: t("workflow.detail.runs.tab"), icon: <HistoryOutlinedIcon /> },
|
|
]}
|
|
renderTabBar={(props, DefaultTabBar) => <DefaultTabBar {...props} style={{ margin: 0 }} />}
|
|
tabBarStyle={{ border: "none" }}
|
|
onChange={(key) => setTabValue(key as typeof tabValue)}
|
|
/>
|
|
</PageHeader>
|
|
</Card>
|
|
|
|
<div className="p-4">
|
|
<Card loading={!initialized}>
|
|
<Show when={tabValue === "orchestration"}>
|
|
<div className="relative">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="flex-1 overflow-hidden">
|
|
<Show when={workflow.hasDraft!}>
|
|
<Alert banner message={<div className="truncate">{t("workflow.detail.orchestration.draft.alert")}</div>} type="warning" />
|
|
</Show>
|
|
</div>
|
|
<div className="flex justify-end">
|
|
<Space>
|
|
<Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={isRunning} type="primary" onClick={handleRunClick}>
|
|
{t("workflow.detail.orchestration.action.run")}
|
|
</Button>
|
|
|
|
<Button.Group>
|
|
<Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}>
|
|
{t("workflow.detail.orchestration.action.release")}
|
|
</Button>
|
|
|
|
<Dropdown
|
|
menu={{
|
|
items: [
|
|
{
|
|
key: "discard",
|
|
disabled: !allowDiscard,
|
|
label: t("workflow.detail.orchestration.action.discard"),
|
|
icon: <UndoOutlinedIcon />,
|
|
onClick: handleDiscardClick,
|
|
},
|
|
],
|
|
}}
|
|
trigger={["click"]}
|
|
>
|
|
<Button color="primary" disabled={!allowDiscard} icon={<EllipsisOutlinedIcon />} variant="outlined" />
|
|
</Dropdown>
|
|
</Button.Group>
|
|
</Space>
|
|
</div>
|
|
</div>
|
|
<div className="px-12 py-8">
|
|
<WorkflowElements />
|
|
</div>
|
|
</div>
|
|
</Show>
|
|
|
|
<Show when={tabValue === "runs"}>
|
|
<WorkflowRuns workflowId={workflowId!} />
|
|
</Show>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
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<z.infer<typeof formSchema>>({
|
|
initialValues: { name: workflow.name, description: workflow.description },
|
|
onSubmit: async (values) => {
|
|
try {
|
|
await workflowState.setBaseInfo(values.name!, values.description!);
|
|
} catch (err) {
|
|
console.error(err);
|
|
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
|
return false;
|
|
}
|
|
},
|
|
});
|
|
|
|
const handleFormFinish = async () => {
|
|
return submitForm();
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{NotificationContextHolder}
|
|
|
|
<ModalForm
|
|
disabled={formPending}
|
|
layout="vertical"
|
|
form={formInst}
|
|
modalProps={{ destroyOnClose: true }}
|
|
okText={t("common.button.save")}
|
|
title={t(`workflow.detail.baseinfo.modal.title`)}
|
|
trigger={trigger}
|
|
width={480}
|
|
{...formProps}
|
|
onFinish={handleFormFinish}
|
|
>
|
|
<Form.Item name="name" label={t("workflow.detail.baseinfo.form.name.label")} rules={[formRule]}>
|
|
<Input placeholder={t("workflow.detail.baseinfo.form.name.placeholder")} />
|
|
</Form.Item>
|
|
|
|
<Form.Item name="description" label={t("workflow.detail.baseinfo.form.description.label")} rules={[formRule]}>
|
|
<Input placeholder={t("workflow.detail.baseinfo.form.description.placeholder")} />
|
|
</Form.Item>
|
|
</ModalForm>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default WorkflowDetail;
|