diff --git a/ui/src/components/workflow/DeployForm.tsx b/ui/src/components/workflow/DeployForm.tsx new file mode 100644 index 00000000..3e0b7883 --- /dev/null +++ b/ui/src/components/workflow/DeployForm.tsx @@ -0,0 +1,26 @@ +import { WorkflowNode } from "@/domain/workflow"; +import { memo } from "react"; +import DeployToAliyunOSS from "./DeployToAliyunOss"; + +export type DeployFormProps = { + data: WorkflowNode; +}; +const DeployForm = ({ data }: DeployFormProps) => { + return getForm(data); +}; + +export default memo(DeployForm); + +const getForm = (data: WorkflowNode) => { + switch (data.config?.providerType) { + case "aliyun-oss": + return ; + case "tencent": + return ; + case "aws": + return ; + default: + return <>; + } +}; + diff --git a/ui/src/components/workflow/DeployPanelBody.tsx b/ui/src/components/workflow/DeployPanelBody.tsx index 5b5b2ff3..8fcdb589 100644 --- a/ui/src/components/workflow/DeployPanelBody.tsx +++ b/ui/src/components/workflow/DeployPanelBody.tsx @@ -1,39 +1,58 @@ import { accessProviders } from "@/domain/access"; import { WorkflowNode } from "@/domain/workflow"; -import { memo } from "react"; +import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import Show from "../Show"; +import DeployForm from "./DeployForm"; +import { DeployTarget, deployTargets } from "@/domain/domain"; type DeployPanelBodyProps = { data: WorkflowNode; }; const DeployPanelBody = ({ data }: DeployPanelBodyProps) => { const { t } = useTranslation(); + + const [providerType, setProviderType] = useState(""); + + useEffect(() => { + if (data.config?.providerType) { + setProviderType(data.config.providerType as string); + } + }, [data]); return ( <> {/* 默认展示服务商列表 */} -
选择服务商
- {accessProviders - .filter((provider) => provider[3] === "apply" || provider[3] === "all") - .reduce((acc: string[][][], provider, index) => { - if (index % 2 === 0) { - acc.push([provider]); - } else { - acc[acc.length - 1].push(provider); - } - return acc; - }, []) - .map((providerRow, rowIndex) => ( -
- {providerRow.map((provider, index) => ( -
- {provider[1]} -
{t(provider[1])}
-
- ))} -
- ))} + }> +
选择服务商
+ {deployTargets + .reduce((acc: DeployTarget[][], provider, index) => { + if (index % 2 === 0) { + acc.push([provider]); + } else { + acc[acc.length - 1].push(provider); + } + return acc; + }, []) + .map((providerRow, rowIndex) => ( +
+ {providerRow.map((provider, index) => ( +
{ + setProviderType(provider.type); + }} + > + {provider.type} +
{t(provider.name)}
+
+ ))} +
+ ))} +
); }; export default memo(DeployPanelBody); + diff --git a/ui/src/components/workflow/DeployToAliyunOss.tsx b/ui/src/components/workflow/DeployToAliyunOss.tsx new file mode 100644 index 00000000..5ce80b7b --- /dev/null +++ b/ui/src/components/workflow/DeployToAliyunOss.tsx @@ -0,0 +1,180 @@ +import { useTranslation } from "react-i18next"; +import { z } from "zod"; + +import { Input } from "@/components/ui/input"; +import { DeployFormProps } from "./DeployForm"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../ui/form"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { WorkflowNode, WorkflowNodeConfig } from "@/domain/workflow"; +import { useWorkflowStore, WorkflowState } from "@/providers/workflow"; +import { useShallow } from "zustand/shallow"; +import { usePanel } from "./PanelProvider"; +import { Button } from "../ui/button"; + +import { useEffect, useState } from "react"; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; +import { SelectLabel } from "@radix-ui/react-select"; + +const selectState = (state: WorkflowState) => ({ + updateNode: state.updateNode, + getWorkflowOuptutBeforeId: state.getWorkflowOuptutBeforeId, +}); +const DeployToAliyunOSS = ({ data }: DeployFormProps) => { + const { updateNode, getWorkflowOuptutBeforeId } = useWorkflowStore(useShallow(selectState)); + const { hidePanel } = usePanel(); + const { t } = useTranslation(); + + const [beforeOutput, setBeforeOutput] = useState([]); + + useEffect(() => { + const rs = getWorkflowOuptutBeforeId(data.id, "certificate"); + console.log(rs); + setBeforeOutput(rs); + }, [data]); + + const formSchema = z.object({ + providerType: z.string(), + certificate: z.string().min(1), + endpoint: z.string().min(1, { + message: t("domain.deployment.form.aliyun_oss_endpoint.placeholder"), + }), + bucket: z.string().min(1, { + message: t("domain.deployment.form.aliyun_oss_bucket.placeholder"), + }), + domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { + message: t("common.errmsg.domain_invalid"), + }), + }); + + let config: WorkflowNodeConfig = { + certificate: "", + providerType: "aliyun-oss", + endpoint: "", + bucket: "", + domain: "", + }; + if (data) config = data.config ?? config; + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + providerType: config.providerType as string, + certificate: config.certificate as string, + endpoint: config.endpoint as string, + bucket: config.bucket as string, + domain: config.domain as string, + }, + }); + + const onSubmit = async (config: z.infer) => { + updateNode({ ...data, config: { ...config } }); + hidePanel(); + }; + + return ( + <> +
+ { + e.stopPropagation(); + form.handleSubmit(onSubmit)(e); + }} + className="space-y-8" + > + ( + + 证书 + + + + + + + )} + /> + ( + + {t("domain.deployment.form.aliyun_oss_endpoint.label")} + + + + + + + )} + /> + + ( + + {t("domain.deployment.form.aliyun_oss_bucket.label")} + + + + + + + )} + /> + + ( + + {t("domain.deployment.form.domain.label")} + + + + + + + )} + /> + +
+ +
+ + + + ); +}; + +export default DeployToAliyunOSS; + diff --git a/ui/src/components/workflow/PanelBody.tsx b/ui/src/components/workflow/PanelBody.tsx index 6945222f..bba04935 100644 --- a/ui/src/components/workflow/PanelBody.tsx +++ b/ui/src/components/workflow/PanelBody.tsx @@ -13,6 +13,8 @@ const PanelBody = ({ data }: PanelBodyProps) => { return ; case WorkflowNodeType.Apply: return ; + case WorkflowNodeType.Deploy: + return ; case WorkflowNodeType.Notify: return ; case WorkflowNodeType.Branch: @@ -28,3 +30,4 @@ const PanelBody = ({ data }: PanelBodyProps) => { }; export default PanelBody; + diff --git a/ui/src/domain/domain.ts b/ui/src/domain/domain.ts index c4b5f683..54679df1 100644 --- a/ui/src/domain/domain.ts +++ b/ui/src/domain/domain.ts @@ -63,34 +63,39 @@ export type Statistic = { disabled: number; }; -type DeployTarget = { +export type DeployTarget = { type: string; provider: string; name: string; icon: string; }; +export const deployTargetList: string[][] = [ + ["aliyun-oss", "common.provider.aliyun.oss", "/imgs/providers/aliyun.svg"], + ["aliyun-cdn", "common.provider.aliyun.cdn", "/imgs/providers/aliyun.svg"], + ["aliyun-dcdn", "common.provider.aliyun.dcdn", "/imgs/providers/aliyun.svg"], + ["aliyun-clb", "common.provider.aliyun.clb", "/imgs/providers/aliyun.svg"], + ["aliyun-alb", "common.provider.aliyun.alb", "/imgs/providers/aliyun.svg"], + ["aliyun-nlb", "common.provider.aliyun.nlb", "/imgs/providers/aliyun.svg"], + ["tencent-cdn", "common.provider.tencent.cdn", "/imgs/providers/tencent.svg"], + ["tencent-ecdn", "common.provider.tencent.ecdn", "/imgs/providers/tencent.svg"], + ["tencent-clb", "common.provider.tencent.clb", "/imgs/providers/tencent.svg"], + ["tencent-cos", "common.provider.tencent.cos", "/imgs/providers/tencent.svg"], + ["tencent-teo", "common.provider.tencent.teo", "/imgs/providers/tencent.svg"], + ["huaweicloud-cdn", "common.provider.huaweicloud.cdn", "/imgs/providers/huaweicloud.svg"], + ["huaweicloud-elb", "common.provider.huaweicloud.elb", "/imgs/providers/huaweicloud.svg"], + ["baiducloud-cdn", "common.provider.baiducloud.cdn", "/imgs/providers/baiducloud.svg"], + ["qiniu-cdn", "common.provider.qiniu.cdn", "/imgs/providers/qiniu.svg"], + ["dogecloud-cdn", "common.provider.dogecloud.cdn", "/imgs/providers/dogecloud.svg"], + ["local", "common.provider.local", "/imgs/providers/local.svg"], + ["ssh", "common.provider.ssh", "/imgs/providers/ssh.svg"], + ["webhook", "common.provider.webhook", "/imgs/providers/webhook.svg"], + ["k8s-secret", "common.provider.kubernetes.secret", "/imgs/providers/k8s.svg"], +]; + export const deployTargetsMap: Map = new Map( - [ - ["aliyun-oss", "common.provider.aliyun.oss", "/imgs/providers/aliyun.svg"], - ["aliyun-cdn", "common.provider.aliyun.cdn", "/imgs/providers/aliyun.svg"], - ["aliyun-dcdn", "common.provider.aliyun.dcdn", "/imgs/providers/aliyun.svg"], - ["aliyun-clb", "common.provider.aliyun.clb", "/imgs/providers/aliyun.svg"], - ["aliyun-alb", "common.provider.aliyun.alb", "/imgs/providers/aliyun.svg"], - ["aliyun-nlb", "common.provider.aliyun.nlb", "/imgs/providers/aliyun.svg"], - ["tencent-cdn", "common.provider.tencent.cdn", "/imgs/providers/tencent.svg"], - ["tencent-ecdn", "common.provider.tencent.ecdn", "/imgs/providers/tencent.svg"], - ["tencent-clb", "common.provider.tencent.clb", "/imgs/providers/tencent.svg"], - ["tencent-cos", "common.provider.tencent.cos", "/imgs/providers/tencent.svg"], - ["tencent-teo", "common.provider.tencent.teo", "/imgs/providers/tencent.svg"], - ["huaweicloud-cdn", "common.provider.huaweicloud.cdn", "/imgs/providers/huaweicloud.svg"], - ["huaweicloud-elb", "common.provider.huaweicloud.elb", "/imgs/providers/huaweicloud.svg"], - ["baiducloud-cdn", "common.provider.baiducloud.cdn", "/imgs/providers/baiducloud.svg"], - ["qiniu-cdn", "common.provider.qiniu.cdn", "/imgs/providers/qiniu.svg"], - ["dogecloud-cdn", "common.provider.dogecloud.cdn", "/imgs/providers/dogecloud.svg"], - ["local", "common.provider.local", "/imgs/providers/local.svg"], - ["ssh", "common.provider.ssh", "/imgs/providers/ssh.svg"], - ["webhook", "common.provider.webhook", "/imgs/providers/webhook.svg"], - ["k8s-secret", "common.provider.kubernetes.secret", "/imgs/providers/k8s.svg"], - ].map(([type, name, icon]) => [type, { type, provider: type.split("-")[0], name, icon }]) + deployTargetList.map(([type, name, icon]) => [type, { type, provider: type.split("-")[0], name, icon }]) ); + +export const deployTargets = deployTargetList.map(([type, name, icon]) => ({ type, provider: type.split("-")[0], name, icon })); + diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 1e3cb2fa..d26c2029 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -2,6 +2,7 @@ import { produce } from "immer"; import { nanoid } from "nanoid"; import { accessProviders } from "./access"; import i18n from "@/i18n"; +import { deployTargets } from "./domain"; export enum WorkflowNodeType { Start = "start", @@ -25,6 +26,52 @@ export const workflowNodeTypeDefaultName: Map = new Ma [WorkflowNodeType.Custom, "自定义"], ]); +export type WorkflowNodeIo = { + name: string; + type: string; + required: boolean; + label: string; + value?: string; + valueSelector?: WorkflowNodeIoValueSelector; +}; + +export type WorkflowNodeIoValueSelector = { + id: string; + name: string; +}; + +export const workflowNodeTypeDefaultInput: Map = new Map([ + [WorkflowNodeType.Apply, []], + [ + WorkflowNodeType.Deploy, + [ + { + name: "certificate", + type: " certificate", + required: true, + label: "证书", + }, + ], + ], + [WorkflowNodeType.Notify, []], +]); + +export const workflowNodeTypeDefaultOutput: Map = new Map([ + [ + WorkflowNodeType.Apply, + [ + { + name: "certificate", + type: "certificate", + required: true, + label: "证书", + }, + ], + ], + [WorkflowNodeType.Deploy, []], + [WorkflowNodeType.Notify, []], +]); + export type WorkflowNodeConfig = Record; export type WorkflowNode = { @@ -32,7 +79,7 @@ export type WorkflowNode = { name: string; type: WorkflowNodeType; - parameters?: WorkflowNodeIo[]; + input?: WorkflowNodeIo[]; config?: WorkflowNodeConfig; configured?: boolean; output?: WorkflowNodeIo[]; @@ -62,6 +109,8 @@ export const newWorkflowNode = (type: WorkflowNodeType, options: NewWorkflowNode config: { providerType: options.providerType, }, + input: workflowNodeTypeDefaultInput.get(type), + output: workflowNodeTypeDefaultOutput.get(type), }; } @@ -202,6 +251,46 @@ export const removeBranch = (node: WorkflowNode | WorkflowBranchNode, branchNode }); }; +// 1 个分支的节点,不应该能获取到相邻分支上节点的输出 +export const getWorkflowOutputBeforeId = (node: WorkflowNode | WorkflowBranchNode, id: string, type: string): WorkflowNode[] => { + const output: WorkflowNode[] = []; + + const traverse = (current: WorkflowNode | WorkflowBranchNode, output: WorkflowNode[]) => { + if (!current) { + return false; + } + if (current.id === id) { + return true; + } + + if (!isWorkflowBranchNode(current) && current.output && current.output.some((io) => io.type === type)) { + output.push({ + ...current, + output: current.output.filter((io) => io.type === type), + }); + } + + if (isWorkflowBranchNode(current)) { + const currentLength = output.length; + console.log(currentLength); + for (const branch of current.branches) { + if (traverse(branch, output)) { + return true; + } + // 如果当前分支没有输出,清空之前的输出 + if (output.length > currentLength) { + output.splice(currentLength); + } + } + } + + return traverse(current.next as WorkflowNode, output); + }; + + traverse(node, output); + return output; +}; + export type WorkflowBranchNode = { id: string; name: string; @@ -210,20 +299,6 @@ export type WorkflowBranchNode = { next?: WorkflowNode | WorkflowBranchNode; }; -export type WorkflowNodeIo = { - name: string; - type: string; - required: boolean; - description?: string; - value?: string; - valueSelector?: WorkflowNodeIoValueSelector; -}; - -export type WorkflowNodeIoValueSelector = { - id: string; - name: string; -}; - type WorkflowwNodeDropdwonItem = { type: WorkflowNodeType; providerType?: string; @@ -243,39 +318,18 @@ export type WorkflowwNodeDropdwonItemIcon = { name: string; }; -const workflowNodeDropdownApplyList: WorkflowwNodeDropdwonItem[] = accessProviders - .filter((item) => { - return item[3] === "apply" || item[3] === "all"; - }) - .map((item) => { - return { - type: WorkflowNodeType.Apply, - providerType: item[0], - name: i18n.t(item[1]), - leaf: true, - icon: { - type: WorkflowwNodeDropdwonItemIconType.Provider, - name: item[2], - }, - }; - }); - -const workflowNodeDropdownDeployList: WorkflowwNodeDropdwonItem[] = accessProviders - .filter((item) => { - return item[3] === "deploy" || item[3] === "all"; - }) - .map((item) => { - return { - type: WorkflowNodeType.Apply, - providerType: item[0], - name: i18n.t(item[1]), - leaf: true, - icon: { - type: WorkflowwNodeDropdwonItemIconType.Provider, - name: item[2], - }, - }; - }); +const workflowNodeDropdownDeployList: WorkflowwNodeDropdwonItem[] = deployTargets.map((item) => { + return { + type: WorkflowNodeType.Apply, + providerType: item.type, + name: i18n.t(item.name), + leaf: true, + icon: { + type: WorkflowwNodeDropdwonItemIconType.Provider, + name: item.icon, + }, + }; +}); export const workflowNodeDropdownList: WorkflowwNodeDropdwonItem[] = [ { @@ -285,7 +339,7 @@ export const workflowNodeDropdownList: WorkflowwNodeDropdwonItem[] = [ type: WorkflowwNodeDropdwonItemIconType.Icon, name: "NotebookPen", }, - children: workflowNodeDropdownApplyList, + leaf: true, }, { type: WorkflowNodeType.Deploy, @@ -315,3 +369,4 @@ export const workflowNodeDropdownList: WorkflowwNodeDropdwonItem[] = [ }, }, ]; + diff --git a/ui/src/providers/workflow/index.ts b/ui/src/providers/workflow/index.ts index 93ada241..d644799d 100644 --- a/ui/src/providers/workflow/index.ts +++ b/ui/src/providers/workflow/index.ts @@ -1,4 +1,14 @@ -import { addBranch, addNode, removeBranch, removeNode, updateNode, WorkflowBranchNode, WorkflowNode, WorkflowNodeType } from "@/domain/workflow"; +import { + addBranch, + addNode, + getWorkflowOutputBeforeId, + removeBranch, + removeNode, + updateNode, + WorkflowBranchNode, + WorkflowNode, + WorkflowNodeType, +} from "@/domain/workflow"; import { create } from "zustand"; export type WorkflowState = { @@ -8,16 +18,17 @@ export type WorkflowState = { addBranch: (branchId: string) => void; removeNode: (nodeId: string) => void; removeBranch: (branchId: string, index: number) => void; + getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[]; }; -export const useWorkflowStore = create((set) => ({ +export const useWorkflowStore = create((set, get) => ({ root: { id: "1", name: "开始", type: WorkflowNodeType.Start, next: { id: "2", - name: "结束", + name: "分支", type: WorkflowNodeType.Branch, branches: [ { @@ -81,4 +92,9 @@ export const useWorkflowStore = create((set) => ({ root: newRoot, }; }), + + getWorkflowOuptutBeforeId: (id: string, type: string) => { + return getWorkflowOutputBeforeId(get().root, id, type); + }, })); +