diff --git a/internal/deployer/factory.go b/internal/deployer/factory.go index 2b8bb8fe..77fd7d77 100644 --- a/internal/deployer/factory.go +++ b/internal/deployer/factory.go @@ -199,7 +199,7 @@ func createDeployer(target string, accessConfig string, deployConfig map[string] ShellEnv: providerLocal.ShellEnvType(maps.GetValueAsString(deployConfig, "shellEnv")), PreCommand: maps.GetValueAsString(deployConfig, "preCommand"), 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"), OutputKeyPath: maps.GetValueAsString(deployConfig, "keyPath"), PfxPassword: maps.GetValueAsString(deployConfig, "pfxPassword"), @@ -259,7 +259,7 @@ func createDeployer(target string, accessConfig string, deployConfig map[string] SshKeyPassphrase: access.KeyPassphrase, PreCommand: maps.GetValueAsString(deployConfig, "preCommand"), 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"), OutputKeyPath: maps.GetValueAsString(deployConfig, "keyPath"), PfxPassword: maps.GetValueAsString(deployConfig, "pfxPassword"), diff --git a/ui/src/components/notification/NotifyTemplate.tsx b/ui/src/components/notification/NotifyTemplate.tsx index cedbbdf6..918ccbd3 100644 --- a/ui/src/components/notification/NotifyTemplate.tsx +++ b/ui/src/components/notification/NotifyTemplate.tsx @@ -3,8 +3,8 @@ import { useTranslation } from "react-i18next"; import { useRequest } from "ahooks"; import { Button, Form, Input, message, notification, Skeleton } from "antd"; import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; import { ClientResponseError } from "pocketbase"; +import { z } from "zod"; import Show from "@/components/Show"; import { useAntdForm } from "@/hooks"; diff --git a/ui/src/components/workflow/node/ApplyNodeForm.tsx b/ui/src/components/workflow/node/ApplyNodeForm.tsx index bf357b93..14a17520 100644 --- a/ui/src/components/workflow/node/ApplyNodeForm.tsx +++ b/ui/src/components/workflow/node/ApplyNodeForm.tsx @@ -353,17 +353,14 @@ const FormFieldDomainsModalForm = ({ setModel({ domains: data?.split(MULTIPLE_INPUT_DELIMITER) }); }, [data]); - const handleFinish = useCallback( - (values: z.infer) => { - onFinish?.( - values.domains - .map((e) => e.trim()) - .filter((e) => !!e) - .join(MULTIPLE_INPUT_DELIMITER) - ); - }, - [onFinish] - ); + const handleFormFinish = (values: z.infer) => { + onFinish?.( + values.domains + .map((e) => e.trim()) + .filter((e) => !!e) + .join(MULTIPLE_INPUT_DELIMITER) + ); + }; return ( @@ -400,17 +397,14 @@ const FormFieldNameserversModalForm = ({ data, trigger, onFinish }: { data: stri setModel({ nameservers: data?.split(MULTIPLE_INPUT_DELIMITER) }); }, [data]); - const handleFinish = useCallback( - (values: z.infer) => { - onFinish?.( - values.nameservers - .map((e) => e.trim()) - .filter((e) => !!e) - .join(MULTIPLE_INPUT_DELIMITER) - ); - }, - [onFinish] - ); + const handleFormFinish = (values: z.infer) => { + onFinish?.( + values.nameservers + .map((e) => e.trim()) + .filter((e) => !!e) + .join(MULTIPLE_INPUT_DELIMITER) + ); + }; return ( diff --git a/ui/src/components/workflow/node/DeployNodeFormLocalFields.tsx b/ui/src/components/workflow/node/DeployNodeFormLocalFields.tsx index d8515d27..25024329 100644 --- a/ui/src/components/workflow/node/DeployNodeFormLocalFields.tsx +++ b/ui/src/components/workflow/node/DeployNodeFormLocalFields.tsx @@ -6,9 +6,9 @@ import { z } from "zod"; import Show from "@/components/Show"; -const FORMAT_PEM = "pem" as const; -const FORMAT_PFX = "pfx" as const; -const FORMAT_JKS = "jks" as const; +const FORMAT_PEM = "PEM" as const; +const FORMAT_PFX = "PFX" as const; +const FORMAT_JKS = "JKS" as const; const SHELLENV_SH = "sh" as const; const SHELLENV_CMD = "cmd" as const; diff --git a/ui/src/components/workflow/node/DeployNodeFormSSHFields.tsx b/ui/src/components/workflow/node/DeployNodeFormSSHFields.tsx index 1bb88dd5..bfdd2f99 100644 --- a/ui/src/components/workflow/node/DeployNodeFormSSHFields.tsx +++ b/ui/src/components/workflow/node/DeployNodeFormSSHFields.tsx @@ -6,9 +6,9 @@ import { z } from "zod"; import Show from "@/components/Show"; -const FORMAT_PEM = "pem" as const; -const FORMAT_PFX = "pfx" as const; -const FORMAT_JKS = "jks" as const; +const FORMAT_PEM = "PEM" as const; +const FORMAT_PFX = "PFX" as const; +const FORMAT_JKS = "JKS" as const; const DeployNodeFormSSHFields = () => { const { t } = useTranslation(); diff --git a/ui/src/components/workflow/run/WorkflowRunDetailDrawer.tsx b/ui/src/components/workflow/run/WorkflowRunDetailDrawer.tsx index 22e3a68c..50ce97fd 100644 --- a/ui/src/components/workflow/run/WorkflowRunDetailDrawer.tsx +++ b/ui/src/components/workflow/run/WorkflowRunDetailDrawer.tsx @@ -32,28 +32,11 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR setOpen(false)}> - - {t("workflow_run.props.status.succeeded")} - - } - /> + {t("workflow_run.props.status.succeeded")}} /> - - {t("workflow_run.props.status.failed")} - - } - description={data!.error} - /> + {t("workflow_run.props.status.failed")}} />
diff --git a/ui/src/components/workflow/run/WorkflowRuns.tsx b/ui/src/components/workflow/run/WorkflowRuns.tsx index b82a8a79..b5521bdc 100644 --- a/ui/src/components/workflow/run/WorkflowRuns.tsx +++ b/ui/src/components/workflow/run/WorkflowRuns.tsx @@ -54,12 +54,12 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { ); } else { - + return ( {t("workflow_run.props.status.failed")} - ; + ); } }, }, diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 85096716..1ec7156c 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -362,20 +362,20 @@ export const getWorkflowOutputBeforeId = (node: WorkflowNode | WorkflowBranchNod return output; }; -export const allNodesValidated = (node: WorkflowNode | WorkflowBranchNode): boolean => { - let current = node; +export const isAllNodesValidated = (node: WorkflowNode | WorkflowBranchNode): boolean => { + let current = node as typeof node | undefined; while (current) { if (!isWorkflowBranchNode(current) && !current.validated) { return false; } if (isWorkflowBranchNode(current)) { for (const branch of current.branches) { - if (!allNodesValidated(branch)) { + if (!isAllNodesValidated(branch)) { return false; } } } - current = current.next as WorkflowNode; + current = current.next; } return true; }; diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index ee3b10de..7bc59ac9 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -7,11 +7,14 @@ "workflow.action.edit": "Edit workflow", "workflow.action.delete": "Delete 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.release": "Release", "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.description": "Description", @@ -35,13 +38,6 @@ "workflow.detail.baseinfo.form.description.placeholder": "Please enter description", "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.delete.label": "Delete Node", "workflow.node.addBranch.label": "Add Branch", diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index 0930d6ec..c0889161 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -11,8 +11,10 @@ "workflow.action.discard.confirm": "确定要撤销更改并回退到最近一次发布的版本吗?", "workflow.action.release": "发布更改", "workflow.action.release.confirm": "确定要发布更改吗?", - "workflow.action.execute": "执行", - "workflow.action.execute.confirm": "确定要立即执行此工作流吗?", + "workflow.action.release.failed.uncompleted": "请先完成流程编排", + "workflow.action.run": "执行", + "workflow.action.run.confirm": "存在未发布的更改,确定要按最近一次发布的版本来执行此工作流吗?", + "workflow.action.enable.failed.uncompleted": "请先完成流程编排并发布更改", "workflow.props.name": "名称", "workflow.props.description": "描述", @@ -36,13 +38,6 @@ "workflow.detail.baseinfo.form.description.placeholder": "请输入工作流描述", "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.delete.label": "删除节点", "workflow.node.addBranch.label": "添加分支", diff --git a/ui/src/pages/login/Login.tsx b/ui/src/pages/login/Login.tsx index c6e885ff..092d8270 100644 --- a/ui/src/pages/login/Login.tsx +++ b/ui/src/pages/login/Login.tsx @@ -28,7 +28,7 @@ const Login = () => { onSubmit: async (values) => { try { await getPocketBase().admins.authWithPassword(values.username, values.password); - navigage("/"); + await navigage("/"); } catch (err) { notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); } diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx index 11afc13a..626ebc32 100644 --- a/ui/src/pages/workflows/WorkflowDetail.tsx +++ b/ui/src/pages/workflows/WorkflowDetail.tsx @@ -1,14 +1,18 @@ import { useEffect, useMemo, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; 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 { PageHeader } from "@ant-design/pro-components"; import { CaretRightOutlined as CaretRightOutlinedIcon, DeleteOutlined as DeleteOutlinedIcon, EllipsisOutlined as EllipsisOutlinedIcon, + UndoOutlined as UndoOutlinedIcon, } from "@ant-design/icons"; +import { ClientResponseError } from "pocketbase"; +import { isEqual } from "radash"; import { z } from "zod"; import Show from "@/components/Show"; @@ -18,9 +22,10 @@ import NodeRender from "@/components/workflow/NodeRender"; import WorkflowRuns from "@/components/workflow/run/WorkflowRuns"; import WorkflowProvider from "@/components/workflow/WorkflowProvider"; 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 { remove as removeWorkflow } from "@/repository/workflow"; +import { run as runWorkflow } from "@/api/workflow"; import { getErrMsg } from "@/utils/error"; const WorkflowDetail = () => { @@ -33,16 +38,17 @@ const WorkflowDetail = () => { const [notificationApi, NotificationContextHolder] = notification.useNotification(); 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(() => { + // TODO: loading init(workflowId); }, [workflowId, init]); const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration"); - // const [running, setRunning] = useState(false); - - const elements = useMemo(() => { + const workflowNodes = useMemo(() => { let current = workflow.draft as WorkflowNode; const elements: JSX.Element[] = []; @@ -58,6 +64,16 @@ const WorkflowDetail = () => { return elements; }, [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) => { try { await setBaseInfo(values.name!, values.description!); @@ -69,10 +85,11 @@ const WorkflowDetail = () => { }; const handleEnableChange = () => { - if (!workflow.enabled && !allNodesValidated(workflow.content!)) { - messageApi.warning(t("workflow.detail.action.save.failed.uncompleted")); + if (!workflow.enabled && !isAllNodesValidated(workflow.content!)) { + messageApi.warning(t("workflow.action.enable.failed.uncompleted")); return; } + switchEnable(); }; @@ -84,7 +101,7 @@ const WorkflowDetail = () => { try { const resp: boolean = await removeWorkflow(workflow); if (resp) { - navigate("/workflows"); + navigate("/workflows", { replace: true }); } } catch (err) { console.error(err); @@ -94,29 +111,59 @@ const WorkflowDetail = () => { }); }; - // TODO: 发布更改 撤销更改 立即执行 - // const handleWorkflowSaveClick = () => { - // if (!allNodesValidated(workflow.draft as WorkflowNode)) { - // messageApi.warning(t("workflow.detail.action.save.failed.uncompleted")); - // return; - // } - // save(); - // }; + const handleDiscardClick = () => { + alert("TODO"); + }; - // const handleRunClick = async () => { - // if (running) return; + const handleReleaseClick = () => { + if (!isAllNodesValidated(workflow.draft!)) { + messageApi.warning(t("workflow.action.release.failed.uncompleted")); + return; + } - // setRunning(true); - // try { - // await runWorkflow(workflow.id as string); - // messageApi.success(t("workflow.detail.action.run.success")); - // } catch (err) { - // console.error(err); - // messageApi.warning(t("workflow.detail.action.run.failed")); - // } finally { - // setRunning(false); - // } - // }; + save(); + + messageApi.success(t("common.text.operation_succeeded")); + }; + + const handleRunClick = () => { + if (!workflow.enabled) { + alert("TODO: 暂时只支持执行已启用的工作流"); + 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 (
@@ -175,17 +222,37 @@ const WorkflowDetail = () => {
- {elements} + {workflowNodes}
- - - - - + + + + + , + onClick: handleDiscardClick, + }, + ], + }} + trigger={["click"]} + > +
@@ -237,7 +304,7 @@ const WorkflowBaseInfoModalForm = ({ }, }); - const handleFinish = async () => { + const handleFormFinish = async () => { return formApi.submit(); }; @@ -252,7 +319,7 @@ const WorkflowBaseInfoModalForm = ({ trigger={trigger} width={480} {...formProps} - onFinish={handleFinish} + onFinish={handleFormFinish} > @@ -264,4 +331,5 @@ const WorkflowBaseInfoModalForm = ({ ); }; + export default WorkflowDetail; diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index 6a0d2e01..1287bd3d 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -25,7 +25,7 @@ import { DeleteOutlined as DeleteOutlinedIcon, EditOutlined as EditOutlinedIcon, import dayjs from "dayjs"; 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 { getErrMsg } from "@/utils/error"; @@ -241,8 +241,8 @@ const WorkflowList = () => { const handleEnabledChange = async (workflow: WorkflowModel) => { try { - if (!workflow.enabled && !allNodesValidated(workflow.content!)) { - messageApi.warning(t("workflow.detail.action.save.failed.uncompleted")); + if (!workflow.enabled && !isAllNodesValidated(workflow.content!)) { + messageApi.warning(t("workflow.action.enable.failed.uncompleted")); return; } diff --git a/ui/src/stores/access/index.ts b/ui/src/stores/access/index.ts index c2248503..5e218366 100644 --- a/ui/src/stores/access/index.ts +++ b/ui/src/stores/access/index.ts @@ -23,6 +23,19 @@ export const useAccessStore = create((set) => { loading: 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) => { const record = await saveAccess(access); set( @@ -58,18 +71,5 @@ export const useAccessStore = create((set) => { 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 }); - } - }, }; }); diff --git a/ui/src/stores/contact/index.ts b/ui/src/stores/contact/index.ts index 957722b5..2d793ba4 100644 --- a/ui/src/stores/contact/index.ts +++ b/ui/src/stores/contact/index.ts @@ -24,6 +24,19 @@ export const useContactStore = create((set, get) => { loading: false, loadedAtOnce: false, + fetchEmails: async () => { + fetcher ??= getSettings(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) => { settings ??= await getSettings(SETTINGS_NAMES.EMAILS); settings = await saveSettings({ @@ -58,18 +71,5 @@ export const useContactStore = create((set, get) => { }); get().setEmails(emails); }, - - fetchEmails: async () => { - fetcher ??= getSettings(SETTINGS_NAMES.EMAILS); - - try { - set({ loading: true }); - settings = await fetcher; - set({ emails: settings.content.emails?.sort() ?? [], loadedAtOnce: true }); - } finally { - fetcher = null; - set({ loading: false }); - } - }, }; }); diff --git a/ui/src/stores/notify/index.ts b/ui/src/stores/notify/index.ts index b2d6db58..eca6a939 100644 --- a/ui/src/stores/notify/index.ts +++ b/ui/src/stores/notify/index.ts @@ -23,6 +23,19 @@ export const useNotifyChannelStore = create((set, get) => { loading: false, loadedAtOnce: false, + fetchChannels: async () => { + fetcher ??= getSettings(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) => { settings ??= await getSettings(SETTINGS_NAMES.NOTIFY_CHANNELS); return get().setChannels( @@ -47,18 +60,5 @@ export const useNotifyChannelStore = create((set, get) => { }) ); }, - - fetchChannels: async () => { - fetcher ??= getSettings(SETTINGS_NAMES.NOTIFY_CHANNELS); - - try { - set({ loading: true }); - settings = await fetcher; - set({ channels: settings.content ?? {}, loadedAtOnce: true }); - } finally { - fetcher = null; - set({ loading: false }); - } - }, }; }); diff --git a/ui/tsconfig.app.json b/ui/tsconfig.app.json index a191ee03..954989fb 100644 --- a/ui/tsconfig.app.json +++ b/ui/tsconfig.app.json @@ -5,9 +5,9 @@ "target": "ES2020", "useDefineForClassFields": true, "lib": [ - "ES2020", "DOM", - "DOM.Iterable" + "DOM.Iterable", + "ESNext", ], "module": "ESNext", "skipLibCheck": true,