mirror of
https://github.com/woodchen-ink/certimate.git
synced 2025-07-18 17:31:55 +08:00
feat(ui): release & run workflow
This commit is contained in:
parent
5c1854948c
commit
6075cc5c95
@ -199,7 +199,7 @@ func createDeployer(target string, accessConfig string, deployConfig map[string]
|
|||||||
ShellEnv: providerLocal.ShellEnvType(maps.GetValueAsString(deployConfig, "shellEnv")),
|
ShellEnv: providerLocal.ShellEnvType(maps.GetValueAsString(deployConfig, "shellEnv")),
|
||||||
PreCommand: maps.GetValueAsString(deployConfig, "preCommand"),
|
PreCommand: maps.GetValueAsString(deployConfig, "preCommand"),
|
||||||
PostCommand: maps.GetValueAsString(deployConfig, "postCommand"),
|
PostCommand: maps.GetValueAsString(deployConfig, "postCommand"),
|
||||||
OutputFormat: providerLocal.OutputFormatType(maps.GetValueOrDefaultAsString(deployConfig, "format", "PEM")),
|
OutputFormat: providerLocal.OutputFormatType(maps.GetValueOrDefaultAsString(deployConfig, "format", string(providerLocal.OUTPUT_FORMAT_PEM))),
|
||||||
OutputCertPath: maps.GetValueAsString(deployConfig, "certPath"),
|
OutputCertPath: maps.GetValueAsString(deployConfig, "certPath"),
|
||||||
OutputKeyPath: maps.GetValueAsString(deployConfig, "keyPath"),
|
OutputKeyPath: maps.GetValueAsString(deployConfig, "keyPath"),
|
||||||
PfxPassword: maps.GetValueAsString(deployConfig, "pfxPassword"),
|
PfxPassword: maps.GetValueAsString(deployConfig, "pfxPassword"),
|
||||||
@ -259,7 +259,7 @@ func createDeployer(target string, accessConfig string, deployConfig map[string]
|
|||||||
SshKeyPassphrase: access.KeyPassphrase,
|
SshKeyPassphrase: access.KeyPassphrase,
|
||||||
PreCommand: maps.GetValueAsString(deployConfig, "preCommand"),
|
PreCommand: maps.GetValueAsString(deployConfig, "preCommand"),
|
||||||
PostCommand: maps.GetValueAsString(deployConfig, "postCommand"),
|
PostCommand: maps.GetValueAsString(deployConfig, "postCommand"),
|
||||||
OutputFormat: providerSSH.OutputFormatType(maps.GetValueOrDefaultAsString(deployConfig, "format", "PEM")),
|
OutputFormat: providerSSH.OutputFormatType(maps.GetValueOrDefaultAsString(deployConfig, "format", string(providerSSH.OUTPUT_FORMAT_PEM))),
|
||||||
OutputCertPath: maps.GetValueAsString(deployConfig, "certPath"),
|
OutputCertPath: maps.GetValueAsString(deployConfig, "certPath"),
|
||||||
OutputKeyPath: maps.GetValueAsString(deployConfig, "keyPath"),
|
OutputKeyPath: maps.GetValueAsString(deployConfig, "keyPath"),
|
||||||
PfxPassword: maps.GetValueAsString(deployConfig, "pfxPassword"),
|
PfxPassword: maps.GetValueAsString(deployConfig, "pfxPassword"),
|
||||||
|
@ -3,8 +3,8 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useRequest } from "ahooks";
|
import { useRequest } from "ahooks";
|
||||||
import { Button, Form, Input, message, notification, Skeleton } from "antd";
|
import { Button, Form, Input, message, notification, Skeleton } from "antd";
|
||||||
import { createSchemaFieldRule } from "antd-zod";
|
import { createSchemaFieldRule } from "antd-zod";
|
||||||
import { z } from "zod";
|
|
||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import Show from "@/components/Show";
|
import Show from "@/components/Show";
|
||||||
import { useAntdForm } from "@/hooks";
|
import { useAntdForm } from "@/hooks";
|
||||||
|
@ -353,17 +353,14 @@ const FormFieldDomainsModalForm = ({
|
|||||||
setModel({ domains: data?.split(MULTIPLE_INPUT_DELIMITER) });
|
setModel({ domains: data?.split(MULTIPLE_INPUT_DELIMITER) });
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const handleFinish = useCallback(
|
const handleFormFinish = (values: z.infer<typeof formSchema>) => {
|
||||||
(values: z.infer<typeof formSchema>) => {
|
onFinish?.(
|
||||||
onFinish?.(
|
values.domains
|
||||||
values.domains
|
.map((e) => e.trim())
|
||||||
.map((e) => e.trim())
|
.filter((e) => !!e)
|
||||||
.filter((e) => !!e)
|
.join(MULTIPLE_INPUT_DELIMITER)
|
||||||
.join(MULTIPLE_INPUT_DELIMITER)
|
);
|
||||||
);
|
};
|
||||||
},
|
|
||||||
[onFinish]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalForm
|
<ModalForm
|
||||||
@ -375,7 +372,7 @@ const FormFieldDomainsModalForm = ({
|
|||||||
trigger={trigger}
|
trigger={trigger}
|
||||||
validateTrigger="onSubmit"
|
validateTrigger="onSubmit"
|
||||||
width={480}
|
width={480}
|
||||||
onFinish={handleFinish}
|
onFinish={handleFormFinish}
|
||||||
>
|
>
|
||||||
<Form.Item name="domains" rules={[formRule]}>
|
<Form.Item name="domains" rules={[formRule]}>
|
||||||
<MultipleInput placeholder={t("workflow_node.apply.form.domains.multiple_input_modal.placeholder")} />
|
<MultipleInput placeholder={t("workflow_node.apply.form.domains.multiple_input_modal.placeholder")} />
|
||||||
@ -400,17 +397,14 @@ const FormFieldNameserversModalForm = ({ data, trigger, onFinish }: { data: stri
|
|||||||
setModel({ nameservers: data?.split(MULTIPLE_INPUT_DELIMITER) });
|
setModel({ nameservers: data?.split(MULTIPLE_INPUT_DELIMITER) });
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const handleFinish = useCallback(
|
const handleFormFinish = (values: z.infer<typeof formSchema>) => {
|
||||||
(values: z.infer<typeof formSchema>) => {
|
onFinish?.(
|
||||||
onFinish?.(
|
values.nameservers
|
||||||
values.nameservers
|
.map((e) => e.trim())
|
||||||
.map((e) => e.trim())
|
.filter((e) => !!e)
|
||||||
.filter((e) => !!e)
|
.join(MULTIPLE_INPUT_DELIMITER)
|
||||||
.join(MULTIPLE_INPUT_DELIMITER)
|
);
|
||||||
);
|
};
|
||||||
},
|
|
||||||
[onFinish]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalForm
|
<ModalForm
|
||||||
@ -422,7 +416,7 @@ const FormFieldNameserversModalForm = ({ data, trigger, onFinish }: { data: stri
|
|||||||
trigger={trigger}
|
trigger={trigger}
|
||||||
validateTrigger="onSubmit"
|
validateTrigger="onSubmit"
|
||||||
width={480}
|
width={480}
|
||||||
onFinish={handleFinish}
|
onFinish={handleFormFinish}
|
||||||
>
|
>
|
||||||
<Form.Item name="nameservers" rules={[formRule]}>
|
<Form.Item name="nameservers" rules={[formRule]}>
|
||||||
<MultipleInput placeholder={t("workflow_node.apply.form.nameservers.multiple_input_modal.placeholder")} />
|
<MultipleInput placeholder={t("workflow_node.apply.form.nameservers.multiple_input_modal.placeholder")} />
|
||||||
|
@ -6,9 +6,9 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import Show from "@/components/Show";
|
import Show from "@/components/Show";
|
||||||
|
|
||||||
const FORMAT_PEM = "pem" as const;
|
const FORMAT_PEM = "PEM" as const;
|
||||||
const FORMAT_PFX = "pfx" as const;
|
const FORMAT_PFX = "PFX" as const;
|
||||||
const FORMAT_JKS = "jks" as const;
|
const FORMAT_JKS = "JKS" as const;
|
||||||
|
|
||||||
const SHELLENV_SH = "sh" as const;
|
const SHELLENV_SH = "sh" as const;
|
||||||
const SHELLENV_CMD = "cmd" as const;
|
const SHELLENV_CMD = "cmd" as const;
|
||||||
|
@ -6,9 +6,9 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import Show from "@/components/Show";
|
import Show from "@/components/Show";
|
||||||
|
|
||||||
const FORMAT_PEM = "pem" as const;
|
const FORMAT_PEM = "PEM" as const;
|
||||||
const FORMAT_PFX = "pfx" as const;
|
const FORMAT_PFX = "PFX" as const;
|
||||||
const FORMAT_JKS = "jks" as const;
|
const FORMAT_JKS = "JKS" as const;
|
||||||
|
|
||||||
const DeployNodeFormSSHFields = () => {
|
const DeployNodeFormSSHFields = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -32,28 +32,11 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR
|
|||||||
<Drawer closable destroyOnClose open={open} loading={loading} placement="right" title={`runlog-${data?.id}`} width={640} onClose={() => setOpen(false)}>
|
<Drawer closable destroyOnClose open={open} loading={loading} placement="right" title={`runlog-${data?.id}`} width={640} onClose={() => setOpen(false)}>
|
||||||
<Show when={!!data}>
|
<Show when={!!data}>
|
||||||
<Show when={data!.succeed}>
|
<Show when={data!.succeed}>
|
||||||
<Alert
|
<Alert showIcon type="success" message={<Typography.Text type="success">{t("workflow_run.props.status.succeeded")}</Typography.Text>} />
|
||||||
showIcon
|
|
||||||
type="success"
|
|
||||||
message={
|
|
||||||
<>
|
|
||||||
<Typography.Text type="success">{t("workflow_run.props.status.succeeded")}</Typography.Text>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!!data!.error}>
|
<Show when={!!data!.error}>
|
||||||
<Alert
|
<Alert showIcon type="error" message={<Typography.Text type="danger">{t("workflow_run.props.status.failed")}</Typography.Text>} />
|
||||||
showIcon
|
|
||||||
type="error"
|
|
||||||
message={
|
|
||||||
<>
|
|
||||||
<Typography.Text type="danger">{t("workflow_run.props.status.failed")}</Typography.Text>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
description={data!.error}
|
|
||||||
/>
|
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div className="mt-4 p-4 bg-black text-stone-200 rounded-md">
|
<div className="mt-4 p-4 bg-black text-stone-200 rounded-md">
|
||||||
|
@ -54,12 +54,12 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
|||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
<Tooltip title={record.error}>
|
return (
|
||||||
<Space>
|
<Space>
|
||||||
<CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />
|
<CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />
|
||||||
<Typography.Text type="danger">{t("workflow_run.props.status.failed")}</Typography.Text>
|
<Typography.Text type="danger">{t("workflow_run.props.status.failed")}</Typography.Text>
|
||||||
</Space>
|
</Space>
|
||||||
</Tooltip>;
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -362,20 +362,20 @@ export const getWorkflowOutputBeforeId = (node: WorkflowNode | WorkflowBranchNod
|
|||||||
return output;
|
return output;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const allNodesValidated = (node: WorkflowNode | WorkflowBranchNode): boolean => {
|
export const isAllNodesValidated = (node: WorkflowNode | WorkflowBranchNode): boolean => {
|
||||||
let current = node;
|
let current = node as typeof node | undefined;
|
||||||
while (current) {
|
while (current) {
|
||||||
if (!isWorkflowBranchNode(current) && !current.validated) {
|
if (!isWorkflowBranchNode(current) && !current.validated) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isWorkflowBranchNode(current)) {
|
if (isWorkflowBranchNode(current)) {
|
||||||
for (const branch of current.branches) {
|
for (const branch of current.branches) {
|
||||||
if (!allNodesValidated(branch)) {
|
if (!isAllNodesValidated(branch)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
current = current.next as WorkflowNode;
|
current = current.next;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
@ -7,11 +7,14 @@
|
|||||||
"workflow.action.edit": "Edit workflow",
|
"workflow.action.edit": "Edit workflow",
|
||||||
"workflow.action.delete": "Delete workflow",
|
"workflow.action.delete": "Delete workflow",
|
||||||
"workflow.action.delete.confirm": "Are you sure to delete this workflow?",
|
"workflow.action.delete.confirm": "Are you sure to delete this workflow?",
|
||||||
"workflow.action.discard": "Discard",
|
"workflow.action.discard": "Discard changes",
|
||||||
"workflow.action.discard.confirm": "Are you sure to discard your changes?",
|
"workflow.action.discard.confirm": "Are you sure to discard your changes?",
|
||||||
"workflow.action.release": "Release",
|
"workflow.action.release": "Release",
|
||||||
"workflow.action.release.confirm": "Are you sure to release your changes?",
|
"workflow.action.release.confirm": "Are you sure to release your changes?",
|
||||||
"workflow.action.execute": "Run",
|
"workflow.action.release.failed.uncompleted": "Please complete the orchestration first",
|
||||||
|
"workflow.action.run": "Run",
|
||||||
|
"workflow.action.run.confirm": "There are unreleased changes, are you sure to run this workflow based on the latest released version?",
|
||||||
|
"workflow.action.enable.failed.uncompleted": "Please complete the orchestration and publish the changes first",
|
||||||
|
|
||||||
"workflow.props.name": "Name",
|
"workflow.props.name": "Name",
|
||||||
"workflow.props.description": "Description",
|
"workflow.props.description": "Description",
|
||||||
@ -35,13 +38,6 @@
|
|||||||
"workflow.detail.baseinfo.form.description.placeholder": "Please enter description",
|
"workflow.detail.baseinfo.form.description.placeholder": "Please enter description",
|
||||||
|
|
||||||
"workflow.common.certificate.label": "Certificate",
|
"workflow.common.certificate.label": "Certificate",
|
||||||
"workflow.detail.action.save": "Save updates",
|
|
||||||
"workflow.detail.action.save.failed": "Save failed",
|
|
||||||
"workflow.detail.action.save.failed.uncompleted": "Please complete the orchestration and publish the changes first",
|
|
||||||
"workflow.detail.action.run": "Run",
|
|
||||||
"workflow.detail.action.run.failed": "Run failed",
|
|
||||||
"workflow.detail.action.run.success": "Run success",
|
|
||||||
"workflow.detail.action.running": "Running",
|
|
||||||
"workflow.node.setting.label": "Setting Node",
|
"workflow.node.setting.label": "Setting Node",
|
||||||
"workflow.node.delete.label": "Delete Node",
|
"workflow.node.delete.label": "Delete Node",
|
||||||
"workflow.node.addBranch.label": "Add Branch",
|
"workflow.node.addBranch.label": "Add Branch",
|
||||||
|
@ -11,8 +11,10 @@
|
|||||||
"workflow.action.discard.confirm": "确定要撤销更改并回退到最近一次发布的版本吗?",
|
"workflow.action.discard.confirm": "确定要撤销更改并回退到最近一次发布的版本吗?",
|
||||||
"workflow.action.release": "发布更改",
|
"workflow.action.release": "发布更改",
|
||||||
"workflow.action.release.confirm": "确定要发布更改吗?",
|
"workflow.action.release.confirm": "确定要发布更改吗?",
|
||||||
"workflow.action.execute": "执行",
|
"workflow.action.release.failed.uncompleted": "请先完成流程编排",
|
||||||
"workflow.action.execute.confirm": "确定要立即执行此工作流吗?",
|
"workflow.action.run": "执行",
|
||||||
|
"workflow.action.run.confirm": "存在未发布的更改,确定要按最近一次发布的版本来执行此工作流吗?",
|
||||||
|
"workflow.action.enable.failed.uncompleted": "请先完成流程编排并发布更改",
|
||||||
|
|
||||||
"workflow.props.name": "名称",
|
"workflow.props.name": "名称",
|
||||||
"workflow.props.description": "描述",
|
"workflow.props.description": "描述",
|
||||||
@ -36,13 +38,6 @@
|
|||||||
"workflow.detail.baseinfo.form.description.placeholder": "请输入工作流描述",
|
"workflow.detail.baseinfo.form.description.placeholder": "请输入工作流描述",
|
||||||
|
|
||||||
"workflow.common.certificate.label": "证书",
|
"workflow.common.certificate.label": "证书",
|
||||||
"workflow.detail.action.save": "保存变更",
|
|
||||||
"workflow.detail.action.save.failed": "保存失败",
|
|
||||||
"workflow.detail.action.save.failed.uncompleted": "请先完成流程编排并发布更改",
|
|
||||||
"workflow.detail.action.run": "立即执行",
|
|
||||||
"workflow.detail.action.run.failed": "执行失败",
|
|
||||||
"workflow.detail.action.run.success": "执行成功",
|
|
||||||
"workflow.detail.action.running": "正在执行",
|
|
||||||
"workflow.node.setting.label": "设置节点",
|
"workflow.node.setting.label": "设置节点",
|
||||||
"workflow.node.delete.label": "删除节点",
|
"workflow.node.delete.label": "删除节点",
|
||||||
"workflow.node.addBranch.label": "添加分支",
|
"workflow.node.addBranch.label": "添加分支",
|
||||||
|
@ -28,7 +28,7 @@ const Login = () => {
|
|||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
try {
|
try {
|
||||||
await getPocketBase().admins.authWithPassword(values.username, values.password);
|
await getPocketBase().admins.authWithPassword(values.username, values.password);
|
||||||
navigage("/");
|
await navigage("/");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, Card, Dropdown, Form, Input, message, Modal, notification, Tabs, Typography } from "antd";
|
import { useDeepCompareEffect } from "ahooks";
|
||||||
|
import { Button, Card, Dropdown, Form, Input, message, Modal, notification, Space, Tabs, Typography } from "antd";
|
||||||
import { createSchemaFieldRule } from "antd-zod";
|
import { createSchemaFieldRule } from "antd-zod";
|
||||||
import { PageHeader } from "@ant-design/pro-components";
|
import { PageHeader } from "@ant-design/pro-components";
|
||||||
import {
|
import {
|
||||||
CaretRightOutlined as CaretRightOutlinedIcon,
|
CaretRightOutlined as CaretRightOutlinedIcon,
|
||||||
DeleteOutlined as DeleteOutlinedIcon,
|
DeleteOutlined as DeleteOutlinedIcon,
|
||||||
EllipsisOutlined as EllipsisOutlinedIcon,
|
EllipsisOutlined as EllipsisOutlinedIcon,
|
||||||
|
UndoOutlined as UndoOutlinedIcon,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
|
import { ClientResponseError } from "pocketbase";
|
||||||
|
import { isEqual } from "radash";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import Show from "@/components/Show";
|
import Show from "@/components/Show";
|
||||||
@ -18,9 +22,10 @@ import NodeRender from "@/components/workflow/NodeRender";
|
|||||||
import WorkflowRuns from "@/components/workflow/run/WorkflowRuns";
|
import WorkflowRuns from "@/components/workflow/run/WorkflowRuns";
|
||||||
import WorkflowProvider from "@/components/workflow/WorkflowProvider";
|
import WorkflowProvider from "@/components/workflow/WorkflowProvider";
|
||||||
import { useAntdForm, useZustandShallowSelector } from "@/hooks";
|
import { useAntdForm, useZustandShallowSelector } from "@/hooks";
|
||||||
import { allNodesValidated, type WorkflowModel, type WorkflowNode } from "@/domain/workflow";
|
import { isAllNodesValidated, type WorkflowModel, type WorkflowNode } from "@/domain/workflow";
|
||||||
import { useWorkflowStore } from "@/stores/workflow";
|
import { useWorkflowStore } from "@/stores/workflow";
|
||||||
import { remove as removeWorkflow } from "@/repository/workflow";
|
import { remove as removeWorkflow } from "@/repository/workflow";
|
||||||
|
import { run as runWorkflow } from "@/api/workflow";
|
||||||
import { getErrMsg } from "@/utils/error";
|
import { getErrMsg } from "@/utils/error";
|
||||||
|
|
||||||
const WorkflowDetail = () => {
|
const WorkflowDetail = () => {
|
||||||
@ -33,16 +38,17 @@ const WorkflowDetail = () => {
|
|||||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||||
|
|
||||||
const { id: workflowId } = useParams();
|
const { id: workflowId } = useParams();
|
||||||
const { workflow, init, setBaseInfo, switchEnable } = useWorkflowStore(useZustandShallowSelector(["workflow", "init", "setBaseInfo", "switchEnable"]));
|
const { workflow, init, save, setBaseInfo, switchEnable } = useWorkflowStore(
|
||||||
|
useZustandShallowSelector(["workflow", "init", "save", "setBaseInfo", "switchEnable"])
|
||||||
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// TODO: loading
|
||||||
init(workflowId);
|
init(workflowId);
|
||||||
}, [workflowId, init]);
|
}, [workflowId, init]);
|
||||||
|
|
||||||
const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration");
|
const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration");
|
||||||
|
|
||||||
// const [running, setRunning] = useState(false);
|
const workflowNodes = useMemo(() => {
|
||||||
|
|
||||||
const elements = useMemo(() => {
|
|
||||||
let current = workflow.draft as WorkflowNode;
|
let current = workflow.draft as WorkflowNode;
|
||||||
|
|
||||||
const elements: JSX.Element[] = [];
|
const elements: JSX.Element[] = [];
|
||||||
@ -58,6 +64,16 @@ const WorkflowDetail = () => {
|
|||||||
return elements;
|
return elements;
|
||||||
}, [workflow]);
|
}, [workflow]);
|
||||||
|
|
||||||
|
const [workflowRunning, setWorkflowRunning] = useState(false);
|
||||||
|
|
||||||
|
const [allowDiscard, setAllowDiscard] = useState(false);
|
||||||
|
const [allowRelease, setAllowRelease] = useState(false);
|
||||||
|
useDeepCompareEffect(() => {
|
||||||
|
const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content);
|
||||||
|
setAllowDiscard(hasChanges && !workflowRunning);
|
||||||
|
setAllowRelease(hasChanges && !workflowRunning);
|
||||||
|
}, [workflow, workflowRunning]);
|
||||||
|
|
||||||
const handleBaseInfoFormFinish = async (values: Pick<WorkflowModel, "name" | "description">) => {
|
const handleBaseInfoFormFinish = async (values: Pick<WorkflowModel, "name" | "description">) => {
|
||||||
try {
|
try {
|
||||||
await setBaseInfo(values.name!, values.description!);
|
await setBaseInfo(values.name!, values.description!);
|
||||||
@ -69,10 +85,11 @@ const WorkflowDetail = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEnableChange = () => {
|
const handleEnableChange = () => {
|
||||||
if (!workflow.enabled && !allNodesValidated(workflow.content!)) {
|
if (!workflow.enabled && !isAllNodesValidated(workflow.content!)) {
|
||||||
messageApi.warning(t("workflow.detail.action.save.failed.uncompleted"));
|
messageApi.warning(t("workflow.action.enable.failed.uncompleted"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switchEnable();
|
switchEnable();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -84,7 +101,7 @@ const WorkflowDetail = () => {
|
|||||||
try {
|
try {
|
||||||
const resp: boolean = await removeWorkflow(workflow);
|
const resp: boolean = await removeWorkflow(workflow);
|
||||||
if (resp) {
|
if (resp) {
|
||||||
navigate("/workflows");
|
navigate("/workflows", { replace: true });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -94,29 +111,59 @@ const WorkflowDetail = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: 发布更改 撤销更改 立即执行
|
const handleDiscardClick = () => {
|
||||||
// const handleWorkflowSaveClick = () => {
|
alert("TODO");
|
||||||
// if (!allNodesValidated(workflow.draft as WorkflowNode)) {
|
};
|
||||||
// messageApi.warning(t("workflow.detail.action.save.failed.uncompleted"));
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// save();
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleRunClick = async () => {
|
const handleReleaseClick = () => {
|
||||||
// if (running) return;
|
if (!isAllNodesValidated(workflow.draft!)) {
|
||||||
|
messageApi.warning(t("workflow.action.release.failed.uncompleted"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// setRunning(true);
|
save();
|
||||||
// try {
|
|
||||||
// await runWorkflow(workflow.id as string);
|
messageApi.success(t("common.text.operation_succeeded"));
|
||||||
// messageApi.success(t("workflow.detail.action.run.success"));
|
};
|
||||||
// } catch (err) {
|
|
||||||
// console.error(err);
|
const handleRunClick = () => {
|
||||||
// messageApi.warning(t("workflow.detail.action.run.failed"));
|
if (!workflow.enabled) {
|
||||||
// } finally {
|
alert("TODO: 暂时只支持执行已启用的工作流");
|
||||||
// setRunning(false);
|
return;
|
||||||
// }
|
}
|
||||||
// };
|
|
||||||
|
const { promise, resolve, reject } = Promise.withResolvers();
|
||||||
|
if (workflow.hasDraft) {
|
||||||
|
modalApi.confirm({
|
||||||
|
title: t("workflow.action.run"),
|
||||||
|
content: t("workflow.action.run.confirm"),
|
||||||
|
onOk: () => resolve(void 0),
|
||||||
|
onCancel: () => reject(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(void 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 异步执行
|
||||||
|
promise.then(async () => {
|
||||||
|
setWorkflowRunning(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await runWorkflow(workflowId!);
|
||||||
|
|
||||||
|
messageApi.warning(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 {
|
||||||
|
setWorkflowRunning(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -175,17 +222,37 @@ const WorkflowDetail = () => {
|
|||||||
<Show when={tabValue === "orchestration"}>
|
<Show when={tabValue === "orchestration"}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="flex flex-col items-center py-12 pr-48">
|
<div className="flex flex-col items-center py-12 pr-48">
|
||||||
<WorkflowProvider>{elements}</WorkflowProvider>
|
<WorkflowProvider>{workflowNodes}</WorkflowProvider>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-0 right-0 z-[1]">
|
<div className="absolute top-0 right-0 z-[1]">
|
||||||
<Button.Group>
|
<Space>
|
||||||
<Button onClick={() => alert("TODO")}>{t("workflow.action.discard")}</Button>
|
<Button icon={<CaretRightOutlinedIcon />} loading={workflowRunning} type="primary" onClick={handleRunClick}>
|
||||||
<Button onClick={() => alert("TODO")}>{t("workflow.action.release")}</Button>
|
{t("workflow.action.run")}
|
||||||
<Button type="primary" onClick={() => alert("TODO")}>
|
|
||||||
<CaretRightOutlinedIcon />
|
|
||||||
{t("workflow.action.execute")}
|
|
||||||
</Button>
|
</Button>
|
||||||
</Button.Group>
|
|
||||||
|
<Button.Group>
|
||||||
|
<Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}>
|
||||||
|
{t("workflow.action.release")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: "discard",
|
||||||
|
disabled: !allowDiscard,
|
||||||
|
label: t("workflow.action.discard"),
|
||||||
|
icon: <UndoOutlinedIcon />,
|
||||||
|
onClick: handleDiscardClick,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
trigger={["click"]}
|
||||||
|
>
|
||||||
|
<Button color="primary" icon={<EllipsisOutlinedIcon />} variant="outlined" />
|
||||||
|
</Dropdown>
|
||||||
|
</Button.Group>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
@ -237,7 +304,7 @@ const WorkflowBaseInfoModalForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFinish = async () => {
|
const handleFormFinish = async () => {
|
||||||
return formApi.submit();
|
return formApi.submit();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -252,7 +319,7 @@ const WorkflowBaseInfoModalForm = ({
|
|||||||
trigger={trigger}
|
trigger={trigger}
|
||||||
width={480}
|
width={480}
|
||||||
{...formProps}
|
{...formProps}
|
||||||
onFinish={handleFinish}
|
onFinish={handleFormFinish}
|
||||||
>
|
>
|
||||||
<Form.Item name="name" label={t("workflow.detail.baseinfo.form.name.label")} rules={[formRule]}>
|
<Form.Item name="name" label={t("workflow.detail.baseinfo.form.name.label")} rules={[formRule]}>
|
||||||
<Input placeholder={t("workflow.detail.baseinfo.form.name.placeholder")} />
|
<Input placeholder={t("workflow.detail.baseinfo.form.name.placeholder")} />
|
||||||
@ -264,4 +331,5 @@ const WorkflowBaseInfoModalForm = ({
|
|||||||
</ModalForm>
|
</ModalForm>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WorkflowDetail;
|
export default WorkflowDetail;
|
||||||
|
@ -25,7 +25,7 @@ import { DeleteOutlined as DeleteOutlinedIcon, EditOutlined as EditOutlinedIcon,
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
|
|
||||||
import { allNodesValidated, type WorkflowModel } from "@/domain/workflow";
|
import { isAllNodesValidated, type WorkflowModel } from "@/domain/workflow";
|
||||||
import { list as listWorkflow, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow";
|
import { list as listWorkflow, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow";
|
||||||
import { getErrMsg } from "@/utils/error";
|
import { getErrMsg } from "@/utils/error";
|
||||||
|
|
||||||
@ -241,8 +241,8 @@ const WorkflowList = () => {
|
|||||||
|
|
||||||
const handleEnabledChange = async (workflow: WorkflowModel) => {
|
const handleEnabledChange = async (workflow: WorkflowModel) => {
|
||||||
try {
|
try {
|
||||||
if (!workflow.enabled && !allNodesValidated(workflow.content!)) {
|
if (!workflow.enabled && !isAllNodesValidated(workflow.content!)) {
|
||||||
messageApi.warning(t("workflow.detail.action.save.failed.uncompleted"));
|
messageApi.warning(t("workflow.action.enable.failed.uncompleted"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +23,19 @@ export const useAccessStore = create<AccessState>((set) => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
loadedAtOnce: false,
|
loadedAtOnce: false,
|
||||||
|
|
||||||
|
fetchAccesses: async () => {
|
||||||
|
fetcher ??= listAccess();
|
||||||
|
|
||||||
|
try {
|
||||||
|
set({ loading: true });
|
||||||
|
const accesses = await fetcher;
|
||||||
|
set({ accesses: accesses ?? [], loadedAtOnce: true });
|
||||||
|
} finally {
|
||||||
|
fetcher = null;
|
||||||
|
set({ loading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
createAccess: async (access) => {
|
createAccess: async (access) => {
|
||||||
const record = await saveAccess(access);
|
const record = await saveAccess(access);
|
||||||
set(
|
set(
|
||||||
@ -58,18 +71,5 @@ export const useAccessStore = create<AccessState>((set) => {
|
|||||||
|
|
||||||
return access as AccessModel;
|
return access as AccessModel;
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchAccesses: async () => {
|
|
||||||
fetcher ??= listAccess();
|
|
||||||
|
|
||||||
try {
|
|
||||||
set({ loading: true });
|
|
||||||
const accesses = await fetcher;
|
|
||||||
set({ accesses: accesses ?? [], loadedAtOnce: true });
|
|
||||||
} finally {
|
|
||||||
fetcher = null;
|
|
||||||
set({ loading: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -24,6 +24,19 @@ export const useContactStore = create<ContactState>((set, get) => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
loadedAtOnce: false,
|
loadedAtOnce: false,
|
||||||
|
|
||||||
|
fetchEmails: async () => {
|
||||||
|
fetcher ??= getSettings<EmailsSettingsContent>(SETTINGS_NAMES.EMAILS);
|
||||||
|
|
||||||
|
try {
|
||||||
|
set({ loading: true });
|
||||||
|
settings = await fetcher;
|
||||||
|
set({ emails: settings.content.emails?.sort() ?? [], loadedAtOnce: true });
|
||||||
|
} finally {
|
||||||
|
fetcher = null;
|
||||||
|
set({ loading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setEmails: async (emails) => {
|
setEmails: async (emails) => {
|
||||||
settings ??= await getSettings<EmailsSettingsContent>(SETTINGS_NAMES.EMAILS);
|
settings ??= await getSettings<EmailsSettingsContent>(SETTINGS_NAMES.EMAILS);
|
||||||
settings = await saveSettings<EmailsSettingsContent>({
|
settings = await saveSettings<EmailsSettingsContent>({
|
||||||
@ -58,18 +71,5 @@ export const useContactStore = create<ContactState>((set, get) => {
|
|||||||
});
|
});
|
||||||
get().setEmails(emails);
|
get().setEmails(emails);
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchEmails: async () => {
|
|
||||||
fetcher ??= getSettings<EmailsSettingsContent>(SETTINGS_NAMES.EMAILS);
|
|
||||||
|
|
||||||
try {
|
|
||||||
set({ loading: true });
|
|
||||||
settings = await fetcher;
|
|
||||||
set({ emails: settings.content.emails?.sort() ?? [], loadedAtOnce: true });
|
|
||||||
} finally {
|
|
||||||
fetcher = null;
|
|
||||||
set({ loading: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -23,6 +23,19 @@ export const useNotifyChannelStore = create<NotifyChannelState>((set, get) => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
loadedAtOnce: false,
|
loadedAtOnce: false,
|
||||||
|
|
||||||
|
fetchChannels: async () => {
|
||||||
|
fetcher ??= getSettings<NotifyChannelsSettingsContent>(SETTINGS_NAMES.NOTIFY_CHANNELS);
|
||||||
|
|
||||||
|
try {
|
||||||
|
set({ loading: true });
|
||||||
|
settings = await fetcher;
|
||||||
|
set({ channels: settings.content ?? {}, loadedAtOnce: true });
|
||||||
|
} finally {
|
||||||
|
fetcher = null;
|
||||||
|
set({ loading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setChannel: async (channel, config) => {
|
setChannel: async (channel, config) => {
|
||||||
settings ??= await getSettings<NotifyChannelsSettingsContent>(SETTINGS_NAMES.NOTIFY_CHANNELS);
|
settings ??= await getSettings<NotifyChannelsSettingsContent>(SETTINGS_NAMES.NOTIFY_CHANNELS);
|
||||||
return get().setChannels(
|
return get().setChannels(
|
||||||
@ -47,18 +60,5 @@ export const useNotifyChannelStore = create<NotifyChannelState>((set, get) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchChannels: async () => {
|
|
||||||
fetcher ??= getSettings<NotifyChannelsSettingsContent>(SETTINGS_NAMES.NOTIFY_CHANNELS);
|
|
||||||
|
|
||||||
try {
|
|
||||||
set({ loading: true });
|
|
||||||
settings = await fetcher;
|
|
||||||
set({ channels: settings.content ?? {}, loadedAtOnce: true });
|
|
||||||
} finally {
|
|
||||||
fetcher = null;
|
|
||||||
set({ loading: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -5,9 +5,9 @@
|
|||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"ES2020",
|
|
||||||
"DOM",
|
"DOM",
|
||||||
"DOM.Iterable"
|
"DOM.Iterable",
|
||||||
|
"ESNext",
|
||||||
],
|
],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user