feat(ui): WorkflowNew page
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
ApartmentOutlined as ApartmentOutlinedIcon,
|
||||
CaretRightOutlined as CaretRightOutlinedIcon,
|
||||
DeleteOutlined as DeleteOutlinedIcon,
|
||||
DownOutlined as DownOutlinedIcon,
|
||||
EllipsisOutlined as EllipsisOutlinedIcon,
|
||||
HistoryOutlined as HistoryOutlinedIcon,
|
||||
UndoOutlined as UndoOutlinedIcon,
|
||||
@@ -45,8 +46,8 @@ const WorkflowDetail = () => {
|
||||
);
|
||||
useEffect(() => {
|
||||
// TODO: loading
|
||||
init(workflowId);
|
||||
}, [workflowId, init]);
|
||||
init(workflowId!);
|
||||
}, [workflowId]);
|
||||
|
||||
const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration");
|
||||
|
||||
@@ -70,10 +71,13 @@ const WorkflowDetail = () => {
|
||||
|
||||
const [allowDiscard, setAllowDiscard] = useState(false);
|
||||
const [allowRelease, setAllowRelease] = useState(false);
|
||||
const [allowRun, setAllowRun] = useState(false);
|
||||
useDeepCompareEffect(() => {
|
||||
const hasReleased = !!workflow.content;
|
||||
const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content);
|
||||
setAllowDiscard(hasChanges && !workflowRunning);
|
||||
setAllowRelease(hasChanges && !workflowRunning);
|
||||
setAllowDiscard(!workflowRunning && hasReleased && hasChanges);
|
||||
setAllowRelease(!workflowRunning && hasChanges);
|
||||
setAllowRun(hasReleased);
|
||||
}, [workflow, workflowRunning]);
|
||||
|
||||
const handleBaseInfoFormFinish = async (values: Pick<WorkflowModel, "name" | "description">) => {
|
||||
@@ -86,13 +90,18 @@ const WorkflowDetail = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnableChange = () => {
|
||||
if (!workflow.enabled && !isAllNodesValidated(workflow.content!)) {
|
||||
const handleEnableChange = async () => {
|
||||
if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) {
|
||||
messageApi.warning(t("workflow.action.enable.failed.uncompleted"));
|
||||
return;
|
||||
}
|
||||
|
||||
switchEnable();
|
||||
try {
|
||||
await switchEnable();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
@@ -114,18 +123,24 @@ const WorkflowDetail = () => {
|
||||
};
|
||||
|
||||
const handleDiscardClick = () => {
|
||||
alert("TODO");
|
||||
modalApi.confirm({
|
||||
title: t("workflow.detail.orchestration.action.discard"),
|
||||
content: t("workflow.detail.orchestration.action.discard.confirm"),
|
||||
onOk: () => {
|
||||
alert("TODO");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleReleaseClick = () => {
|
||||
if (!isAllNodesValidated(workflow.draft!)) {
|
||||
messageApi.warning(t("workflow.action.release.failed.uncompleted"));
|
||||
messageApi.warning(t("workflow.detail.orchestration.action.release.failed.uncompleted"));
|
||||
return;
|
||||
}
|
||||
|
||||
modalApi.confirm({
|
||||
title: t("workflow.action.release"),
|
||||
content: t("workflow.action.release.confirm"),
|
||||
title: t("workflow.detail.orchestration.action.release"),
|
||||
content: t("workflow.detail.orchestration.action.release.confirm"),
|
||||
onOk: async () => {
|
||||
try {
|
||||
await save();
|
||||
@@ -148,8 +163,8 @@ const WorkflowDetail = () => {
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
if (workflow.hasDraft) {
|
||||
modalApi.confirm({
|
||||
title: t("workflow.action.run"),
|
||||
content: t("workflow.action.run.confirm"),
|
||||
title: t("workflow.detail.orchestration.action.run"),
|
||||
content: t("workflow.detail.orchestration.action.run.confirm"),
|
||||
onOk: () => resolve(void 0),
|
||||
onCancel: () => reject(),
|
||||
});
|
||||
@@ -164,7 +179,7 @@ const WorkflowDetail = () => {
|
||||
try {
|
||||
await runWorkflow(workflowId!);
|
||||
|
||||
messageApi.warning(t("common.text.operation_succeeded"));
|
||||
messageApi.success(t("common.text.operation_succeeded"));
|
||||
} catch (err) {
|
||||
if (err instanceof ClientResponseError && err.isAbort) {
|
||||
return;
|
||||
@@ -189,30 +204,33 @@ const WorkflowDetail = () => {
|
||||
style={{ paddingBottom: 0 }}
|
||||
title={workflow.name}
|
||||
extra={[
|
||||
<Button.Group key="actions">
|
||||
<WorkflowBaseInfoModalForm data={workflow} trigger={<Button>{t("common.button.edit")}</Button>} onFinish={handleBaseInfoFormFinish} />
|
||||
<WorkflowBaseInfoModalForm key="edit" data={workflow} trigger={<Button>{t("common.button.edit")}</Button>} onFinish={handleBaseInfoFormFinish} />,
|
||||
|
||||
<Button onClick={handleEnableChange}>{workflow.enabled ? t("common.button.disable") : t("common.button.enable")}</Button>
|
||||
<Button key="enable" onClick={handleEnableChange}>
|
||||
{workflow.enabled ? t("workflow.action.disable") : t("workflow.action.enable")}
|
||||
</Button>,
|
||||
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "delete",
|
||||
label: t("common.button.delete"),
|
||||
danger: true,
|
||||
icon: <DeleteOutlinedIcon />,
|
||||
onClick: () => {
|
||||
handleDeleteClick();
|
||||
},
|
||||
<Dropdown
|
||||
key="more"
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "delete",
|
||||
label: t("workflow.action.delete"),
|
||||
danger: true,
|
||||
icon: <DeleteOutlinedIcon />,
|
||||
onClick: () => {
|
||||
handleDeleteClick();
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button icon={<EllipsisOutlinedIcon />} />
|
||||
</Dropdown>
|
||||
</Button.Group>,
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button icon={<DownOutlinedIcon />} iconPosition="end">
|
||||
{t("common.button.more")}
|
||||
</Button>
|
||||
</Dropdown>,
|
||||
]}
|
||||
>
|
||||
<Typography.Paragraph type="secondary">{workflow.description}</Typography.Paragraph>
|
||||
@@ -239,13 +257,13 @@ const WorkflowDetail = () => {
|
||||
</div>
|
||||
<div className="absolute top-0 right-0 z-[1]">
|
||||
<Space>
|
||||
<Button icon={<CaretRightOutlinedIcon />} loading={workflowRunning} type="primary" onClick={handleRunClick}>
|
||||
{t("workflow.action.run")}
|
||||
<Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={workflowRunning} type="primary" onClick={handleRunClick}>
|
||||
{t("workflow.detail.orchestration.action.run")}
|
||||
</Button>
|
||||
|
||||
<Button.Group>
|
||||
<Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}>
|
||||
{t("workflow.action.release")}
|
||||
{t("workflow.detail.orchestration.action.release")}
|
||||
</Button>
|
||||
|
||||
<Dropdown
|
||||
@@ -254,7 +272,7 @@ const WorkflowDetail = () => {
|
||||
{
|
||||
key: "discard",
|
||||
disabled: !allowDiscard,
|
||||
label: t("workflow.action.discard"),
|
||||
label: t("workflow.detail.orchestration.action.discard"),
|
||||
icon: <UndoOutlinedIcon />,
|
||||
onClick: handleDiscardClick,
|
||||
},
|
||||
|
||||
@@ -245,7 +245,7 @@ const WorkflowList = () => {
|
||||
|
||||
const handleEnabledChange = async (workflow: WorkflowModel) => {
|
||||
try {
|
||||
if (!workflow.enabled && !isAllNodesValidated(workflow.content!)) {
|
||||
if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) {
|
||||
messageApi.warning(t("workflow.action.enable.failed.uncompleted"));
|
||||
return;
|
||||
}
|
||||
|
||||
123
ui/src/pages/workflows/WorkflowNew.tsx
Normal file
123
ui/src/pages/workflows/WorkflowNew.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { PageHeader } from "@ant-design/pro-components";
|
||||
import { Card, Col, Row, Spin, Typography, notification } from "antd";
|
||||
import { sleep } from "radash";
|
||||
|
||||
import { type WorkflowModel, initWorkflow } from "@/domain/workflow";
|
||||
import { save as saveWorkflow } from "@/repository/workflow";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
const TEMPLATE_KEY_BLANK = "blank" as const;
|
||||
const TEMPLATE_KEY_STANDARD = "standard" as const;
|
||||
type TemplateKeys = typeof TEMPLATE_KEY_BLANK | typeof TEMPLATE_KEY_STANDARD;
|
||||
|
||||
const WorkflowNew = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
|
||||
const templateGridSpans = {
|
||||
xs: { flex: "100%" },
|
||||
md: { flex: "100%" },
|
||||
lg: { flex: "50%" },
|
||||
xl: { flex: "50%" },
|
||||
xxl: { flex: "50%" },
|
||||
};
|
||||
const [templateSelectKey, setTemplateSelectKey] = useState<TemplateKeys>();
|
||||
|
||||
const handleTemplateSelect = async (key: TemplateKeys) => {
|
||||
if (templateSelectKey) return;
|
||||
|
||||
setTemplateSelectKey(key);
|
||||
|
||||
try {
|
||||
let workflow: WorkflowModel;
|
||||
|
||||
switch (key) {
|
||||
case TEMPLATE_KEY_BLANK:
|
||||
workflow = initWorkflow();
|
||||
break;
|
||||
|
||||
case TEMPLATE_KEY_STANDARD:
|
||||
workflow = initWorkflow({ template: "standard" });
|
||||
break;
|
||||
|
||||
default:
|
||||
throw "Invalid args: `key`";
|
||||
}
|
||||
|
||||
workflow = await saveWorkflow(workflow);
|
||||
await sleep(500);
|
||||
|
||||
await navigate(`/workflows/${workflow.id}`, { replace: true });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
|
||||
setTemplateSelectKey(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{NotificationContextHolder}
|
||||
|
||||
<Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }}>
|
||||
<PageHeader title={t("workflow.new.title")}>
|
||||
<Typography.Paragraph type="secondary">{t("workflow.new.subtitle")}</Typography.Paragraph>
|
||||
</PageHeader>
|
||||
</Card>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="max-w-[960px] mx-auto px-2">
|
||||
<Typography.Text type="secondary">
|
||||
<div className="mt-4 mb-8 text-xl">{t("workflow.new.templates.title")}</div>
|
||||
</Typography.Text>
|
||||
|
||||
<Row className="justify-stretch" gutter={[16, 16]}>
|
||||
<Col {...templateGridSpans}>
|
||||
<Card
|
||||
className="size-full"
|
||||
cover={<img className="min-h-[120px] object-contain" src="/imgs/workflow/tpl-standard.png" />}
|
||||
hoverable
|
||||
onClick={() => handleTemplateSelect(TEMPLATE_KEY_STANDARD)}
|
||||
>
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<Card.Meta
|
||||
className="flex-grow"
|
||||
title={t("workflow.new.templates.template.standard.title")}
|
||||
description={t("workflow.new.templates.template.standard.description")}
|
||||
/>
|
||||
<Spin spinning={templateSelectKey === TEMPLATE_KEY_STANDARD} />
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col {...templateGridSpans}>
|
||||
<Card
|
||||
className="size-full"
|
||||
cover={<img className="min-h-[120px] object-contain" src="/imgs/workflow/tpl-blank.png" />}
|
||||
hoverable
|
||||
onClick={() => handleTemplateSelect(TEMPLATE_KEY_BLANK)}
|
||||
>
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<Card.Meta
|
||||
className="flex-grow"
|
||||
title={t("workflow.new.templates.template.blank.title")}
|
||||
description={t("workflow.new.templates.template.blank.description")}
|
||||
/>
|
||||
<Spin spinning={templateSelectKey === TEMPLATE_KEY_BLANK} />
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowNew;
|
||||
Reference in New Issue
Block a user