From c6a8f923e4bf311069621fe30cd17a904ec4843f Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 2 Jan 2025 20:24:16 +0800 Subject: [PATCH] feat(ui): WorkflowNew page --- internal/domain/workflow.go | 10 +- internal/domain/workflow_output.go | 2 +- internal/repository/workflow_output.go | 2 +- ui/public/imgs/workflow/tpl-blank.png | Bin 0 -> 5262 bytes ui/public/imgs/workflow/tpl-standard.png | Bin 0 -> 12858 bytes ui/src/components/core/Version.tsx | 4 +- ui/src/components/workflow/AddNode.tsx | 4 +- .../workflow/DropdownMenuItemIcon.tsx | 19 +- ui/src/domain/certificate.ts | 2 +- ui/src/domain/workflow.ts | 247 ++++++++++-------- ui/src/domain/workflowRun.ts | 11 +- ui/src/global.d.ts | 4 +- ui/src/i18n/index.ts | 2 +- ui/src/i18n/locales/en/nls.common.json | 3 +- ui/src/i18n/locales/en/nls.workflow.json | 27 +- ui/src/i18n/locales/zh/nls.common.json | 3 +- ui/src/i18n/locales/zh/nls.workflow.json | 27 +- ui/src/pages/workflows/WorkflowDetail.tsx | 96 ++++--- ui/src/pages/workflows/WorkflowList.tsx | 2 +- ui/src/pages/workflows/WorkflowNew.tsx | 123 +++++++++ ui/src/stores/workflow/index.ts | 52 ++-- 21 files changed, 415 insertions(+), 225 deletions(-) create mode 100644 ui/public/imgs/workflow/tpl-blank.png create mode 100644 ui/public/imgs/workflow/tpl-standard.png create mode 100644 ui/src/pages/workflows/WorkflowNew.tsx diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 2190ba6d..8d342fe2 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -37,8 +37,8 @@ type WorkflowNode struct { Name string `json:"name"` Next *WorkflowNode `json:"next"` Config map[string]any `json:"config"` - Input []WorkflowNodeIo `json:"input"` - Output []WorkflowNodeIo `json:"output"` + Input []WorkflowNodeIO `json:"input"` + Output []WorkflowNodeIO `json:"output"` Validated bool `json:"validated"` Type string `json:"type"` @@ -76,16 +76,16 @@ func (n *WorkflowNode) GetConfigInt64(key string) int64 { return 0 } -type WorkflowNodeIo struct { +type WorkflowNodeIO struct { Label string `json:"label"` Name string `json:"name"` Type string `json:"type"` Required bool `json:"required"` Value any `json:"value"` - ValueSelector WorkflowNodeIoValueSelector `json:"valueSelector"` + ValueSelector WorkflowNodeIOValueSelector `json:"valueSelector"` } -type WorkflowNodeIoValueSelector struct { +type WorkflowNodeIOValueSelector struct { Id string `json:"id"` Name string `json:"name"` } diff --git a/internal/domain/workflow_output.go b/internal/domain/workflow_output.go index 5ae09205..7ce0a005 100644 --- a/internal/domain/workflow_output.go +++ b/internal/domain/workflow_output.go @@ -7,6 +7,6 @@ type WorkflowOutput struct { Workflow string `json:"workflow"` NodeId string `json:"nodeId"` Node *WorkflowNode `json:"node"` - Output []WorkflowNodeIo `json:"output"` + Output []WorkflowNodeIO `json:"output"` Succeed bool `json:"succeed"` } diff --git a/internal/repository/workflow_output.go b/internal/repository/workflow_output.go index a5159a2c..cf99d82f 100644 --- a/internal/repository/workflow_output.go +++ b/internal/repository/workflow_output.go @@ -35,7 +35,7 @@ func (w *WorkflowOutputRepository) Get(ctx context.Context, nodeId string) (*dom return nil, errors.New("failed to unmarshal node") } - output := make([]domain.WorkflowNodeIo, 0) + output := make([]domain.WorkflowNodeIO, 0) if err := record.UnmarshalJSONField("output", &output); err != nil { return nil, errors.New("failed to unmarshal output") } diff --git a/ui/public/imgs/workflow/tpl-blank.png b/ui/public/imgs/workflow/tpl-blank.png new file mode 100644 index 0000000000000000000000000000000000000000..8f683ce6fcb0f5e62d469d415c6277fccf7e0770 GIT binary patch literal 5262 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 literal 0 HcmV?d00001 diff --git a/ui/public/imgs/workflow/tpl-standard.png b/ui/public/imgs/workflow/tpl-standard.png new file mode 100644 index 0000000000000000000000000000000000000000..46698a875c3b835a18b4fb9b059d405c905bd181 GIT binary patch literal 12858 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¨?LE_}115n{4ET98mJ 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 {
- + {t("common.menu.document")}
diff --git a/ui/src/components/workflow/AddNode.tsx b/ui/src/components/workflow/AddNode.tsx index 758e9b3a..b5f96343 100644 --- a/ui/src/components/workflow/AddNode.tsx +++ b/ui/src/components/workflow/AddNode.tsx @@ -1,7 +1,7 @@ import { PlusOutlined as PlusOutlinedIcon } from "@ant-design/icons"; import { Dropdown } from "antd"; -import { newWorkflowNode, workflowNodeDropdownList, type WorkflowNodeType } from "@/domain/workflow"; +import { type WorkflowNodeType, newNode, workflowNodeDropdownList } from "@/domain/workflow"; import { useZustandShallowSelector } from "@/hooks"; import { useWorkflowStore } from "@/stores/workflow"; @@ -12,7 +12,7 @@ const AddNode = ({ data }: NodeProps | BrandNodeProps) => { const { addNode } = useWorkflowStore(useZustandShallowSelector(["addNode"])); const handleTypeSelected = (type: WorkflowNodeType, provider?: string) => { - const node = newWorkflowNode(type, { + const node = newNode(type, { providerType: provider, }); diff --git a/ui/src/components/workflow/DropdownMenuItemIcon.tsx b/ui/src/components/workflow/DropdownMenuItemIcon.tsx index 36b2d8d0..67fa4e21 100644 --- a/ui/src/components/workflow/DropdownMenuItemIcon.tsx +++ b/ui/src/components/workflow/DropdownMenuItemIcon.tsx @@ -1,11 +1,18 @@ -import { CloudUpload, GitFork, Megaphone, NotebookPen } from "lucide-react"; +import { + CloudUploadOutlined as CloudUploadOutlinedIcon, + SendOutlined as SendOutlinedIcon, + SisternodeOutlined as SisternodeOutlinedIcon, + SolutionOutlined as SolutionOutlinedIcon, +} from "@ant-design/icons"; +import { Avatar } from "antd"; + import { type WorkflowNodeDropdwonItemIcon, WorkflowNodeDropdwonItemIconType } from "@/domain/workflow"; const icons = new Map([ - ["NotebookPen", ], - ["CloudUpload", ], - ["GitFork", ], - ["Megaphone", ], + ["ApplyNodeIcon", ], + ["DeployNodeIcon", ], + ["BranchNodeIcon", ], + ["NotifyNodeIcon", ], ]); const DropdownMenuItemIcon = ({ type, name }: WorkflowNodeDropdwonItemIcon) => { @@ -13,7 +20,7 @@ const DropdownMenuItemIcon = ({ type, name }: WorkflowNodeDropdwonItemIcon) => { if (type === WorkflowNodeDropdwonItemIconType.Icon) { return icons.get(name); } else { - return ; + return ; } }; diff --git a/ui/src/domain/certificate.ts b/ui/src/domain/certificate.ts index 796b5429..9a399bdb 100644 --- a/ui/src/domain/certificate.ts +++ b/ui/src/domain/certificate.ts @@ -8,7 +8,7 @@ export interface CertificateModel extends BaseModel { certUrl: string; certStableUrl: string; output: string; - expireAt: string; + expireAt: ISO8601String; workflow: string; nodeId: string; expand: { diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 82b68ea6..1633b60d 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -1,16 +1,10 @@ +import dayjs from "dayjs"; import { produce } from "immer"; import { nanoid } from "nanoid"; import i18n from "@/i18n"; import { deployProvidersMap } from "./provider"; -export type WorkflowOutput = { - time: string; - title: string; - content: string; - error: string; -}; - export interface WorkflowModel extends BaseModel { name: string; description?: string; @@ -22,6 +16,7 @@ export interface WorkflowModel extends BaseModel { hasDraft?: boolean; } +// #region Node export enum WorkflowNodeType { Start = "start", End = "end", @@ -33,7 +28,7 @@ export enum WorkflowNodeType { Custom = "custom", } -export const workflowNodeTypeDefaultName: Map = new Map([ +const workflowNodeTypeDefaultNames: Map = new Map([ [WorkflowNodeType.Start, i18n.t("workflow_node.start.label")], [WorkflowNodeType.End, i18n.t("workflow_node.end.label")], [WorkflowNodeType.Branch, i18n.t("workflow_node.branch.label")], @@ -44,21 +39,7 @@ export const workflowNodeTypeDefaultName: Map = new Ma [WorkflowNodeType.Custom, i18n.t("workflow_node.custom.title")], ]); -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([ +const workflowNodeTypeDefaultInputs: Map = new Map([ [WorkflowNodeType.Apply, []], [ WorkflowNodeType.Deploy, @@ -74,7 +55,7 @@ export const workflowNodeTypeDefaultInput: Map = new Map([ +const workflowNodeTypeDefaultOutputs: Map = new Map([ [ WorkflowNodeType.Apply, [ @@ -90,88 +71,122 @@ export const workflowNodeTypeDefaultOutput: Map; - export type WorkflowNode = { id: string; name: string; type: WorkflowNodeType; - validated?: boolean; - input?: WorkflowNodeIo[]; - config?: WorkflowNodeConfig; - output?: WorkflowNodeIo[]; + config?: Record; + input?: WorkflowNodeIO[]; + output?: WorkflowNodeIO[]; + + next?: WorkflowNode | WorkflowBranchNode; + branches?: WorkflowNode[]; + + validated?: boolean; +}; + +/** + * @deprecated + */ +export type WorkflowBranchNode = { + id: string; + name: string; + type: WorkflowNodeType.Branch; + + branches: WorkflowNode[]; next?: WorkflowNode | WorkflowBranchNode; }; -type NewWorkflowNodeOptions = { +export type WorkflowNodeIO = { + name: string; + type: string; + required: boolean; + label: string; + value?: string; + valueSelector?: WorkflowNodeIOValueSelector; +}; + +export type WorkflowNodeIOValueSelector = { + id: string; + name: string; +}; +// #endregion + +type InitWorkflowOptions = { + template?: "standard"; +}; + +export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel => { + const root = newNode(WorkflowNodeType.Start, {}) as WorkflowNode; + root.config = { executionMethod: "manual" }; + + if (options.template === "standard") { + let temp = root; + temp.next = newNode(WorkflowNodeType.Apply, {}); + + temp = temp.next; + temp.next = newNode(WorkflowNodeType.Deploy, {}); + + temp = temp.next; + temp.next = newNode(WorkflowNodeType.Notify, {}); + } + + return { + id: null!, + name: `MyWorkflow-${dayjs().format("YYYYMMDDHHmmss")}`, + type: root.config!.executionMethod as string, + crontab: root.config!.crontab as string, + enabled: false, + draft: root, + hasDraft: true, + created: new Date().toISOString(), + updated: new Date().toISOString(), + }; +}; + +type NewNodeOptions = { branchIndex?: number; providerType?: string; }; -export const initWorkflow = (): WorkflowModel => { - // 开始节点 - const rs = newWorkflowNode(WorkflowNodeType.Start, {}); - let root = rs; +export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions): WorkflowNode | WorkflowBranchNode => { + const nodeTypeName = workflowNodeTypeDefaultNames.get(nodeType) || ""; + const nodeName = options.branchIndex != null ? `${nodeTypeName} ${options.branchIndex + 1}` : nodeTypeName; - // 申请节点 - root.next = newWorkflowNode(WorkflowNodeType.Apply, {}); - root = root.next; - - // 部署节点 - root.next = newWorkflowNode(WorkflowNodeType.Deploy, {}); - root = root.next; - - // 通知节点 - root.next = newWorkflowNode(WorkflowNodeType.Notify, {}); - - return { - id: "", - name: i18n.t("workflow.props.name.default"), - type: "auto", - crontab: "0 0 * * *", - enabled: false, - draft: rs, - created: new Date().toUTCString(), - updated: new Date().toUTCString(), - }; -}; - -export const newWorkflowNode = (type: WorkflowNodeType, options: NewWorkflowNodeOptions): WorkflowNode | WorkflowBranchNode => { - const id = nanoid(); - const typeName = workflowNodeTypeDefaultName.get(type) || ""; - const name = options.branchIndex !== undefined ? `${typeName} ${options.branchIndex + 1}` : typeName; - - let rs: WorkflowNode | WorkflowBranchNode = { - id, - name, - type, + const node: WorkflowNode | WorkflowBranchNode = { + id: nanoid(), + name: nodeName, + type: nodeType, }; - if (type === WorkflowNodeType.Apply || type === WorkflowNodeType.Deploy) { - rs = { - ...rs, - config: { - providerType: options.providerType, - }, - input: workflowNodeTypeDefaultInput.get(type), - output: workflowNodeTypeDefaultOutput.get(type), - }; + switch (nodeType) { + case WorkflowNodeType.Apply: + case WorkflowNodeType.Deploy: + { + node.config = { + providerType: options.providerType, + }; + node.input = workflowNodeTypeDefaultInputs.get(nodeType); + node.output = workflowNodeTypeDefaultOutputs.get(nodeType); + } + break; + + case WorkflowNodeType.Condition: + { + node.validated = true; + } + break; + + case WorkflowNodeType.Branch: + { + node.branches = [newNode(WorkflowNodeType.Condition, { branchIndex: 0 }), newNode(WorkflowNodeType.Condition, { branchIndex: 1 })]; + } + break; } - if (type == WorkflowNodeType.Condition) { - rs.validated = true; - } - - if (type === WorkflowNodeType.Branch) { - rs = { - ...rs, - branches: [newWorkflowNode(WorkflowNodeType.Condition, { branchIndex: 0 }), newWorkflowNode(WorkflowNodeType.Condition, { branchIndex: 1 })], - }; - } - - return rs; + return node; }; export const isWorkflowBranchNode = (node: WorkflowNode | WorkflowBranchNode): node is WorkflowBranchNode => { @@ -226,7 +241,7 @@ export const addBranch = (node: WorkflowNode | WorkflowBranchNode, branchNodeId: return draft; } current.branches.push( - newWorkflowNode(WorkflowNodeType.Condition, { + newNode(WorkflowNodeType.Condition, { branchIndex: current.branches.length, }) ); @@ -340,21 +355,24 @@ export const getWorkflowOutputBeforeId = (node: WorkflowNode | WorkflowBranchNod return output; }; -export const isAllNodesValidated = (node: WorkflowNode | WorkflowBranchNode): boolean => { +export const isAllNodesValidated = (node: WorkflowNode): 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 (current.type === WorkflowNodeType.Branch) { + for (const branch of current.branches!) { if (!isAllNodesValidated(branch)) { return false; } } + } else { + if (!current.validated) { + return false; + } } + current = current.next; } + return true; }; @@ -372,14 +390,9 @@ export const getExecuteMethod = (node: WorkflowNode): { type: string; crontab: s } }; -export type WorkflowBranchNode = { - id: string; - name: string; - type: WorkflowNodeType; - branches: WorkflowNode[]; - next?: WorkflowNode | WorkflowBranchNode; -}; - +/** + * @deprecated + */ type WorkflowNodeDropdwonItem = { type: WorkflowNodeType; providerType?: string; @@ -389,16 +402,25 @@ type WorkflowNodeDropdwonItem = { children?: WorkflowNodeDropdwonItem[]; }; +/** + * @deprecated + */ export enum WorkflowNodeDropdwonItemIconType { Icon, Provider, } +/** + * @deprecated + */ export type WorkflowNodeDropdwonItemIcon = { type: WorkflowNodeDropdwonItemIconType; name: string; }; +/** + * @deprecated + */ const workflowNodeDropdownDeployList: WorkflowNodeDropdwonItem[] = Array.from(deployProvidersMap.values()).map((item) => { return { type: WorkflowNodeType.Apply, @@ -412,41 +434,44 @@ const workflowNodeDropdownDeployList: WorkflowNodeDropdwonItem[] = Array.from(de }; }); +/** + * @deprecated + */ export const workflowNodeDropdownList: WorkflowNodeDropdwonItem[] = [ { type: WorkflowNodeType.Apply, - name: workflowNodeTypeDefaultName.get(WorkflowNodeType.Apply) ?? "", + name: workflowNodeTypeDefaultNames.get(WorkflowNodeType.Apply) ?? "", icon: { type: WorkflowNodeDropdwonItemIconType.Icon, - name: "NotebookPen", + name: "ApplyNodeIcon", }, leaf: true, }, { type: WorkflowNodeType.Deploy, - name: workflowNodeTypeDefaultName.get(WorkflowNodeType.Deploy) ?? "", + name: workflowNodeTypeDefaultNames.get(WorkflowNodeType.Deploy) ?? "", icon: { type: WorkflowNodeDropdwonItemIconType.Icon, - name: "CloudUpload", + name: "DeployNodeIcon", }, children: workflowNodeDropdownDeployList, }, { type: WorkflowNodeType.Branch, - name: workflowNodeTypeDefaultName.get(WorkflowNodeType.Branch) ?? "", + name: workflowNodeTypeDefaultNames.get(WorkflowNodeType.Branch) ?? "", leaf: true, icon: { type: WorkflowNodeDropdwonItemIconType.Icon, - name: "GitFork", + name: "BranchNodeIcon", }, }, { type: WorkflowNodeType.Notify, - name: workflowNodeTypeDefaultName.get(WorkflowNodeType.Notify) ?? "", + name: workflowNodeTypeDefaultNames.get(WorkflowNodeType.Notify) ?? "", leaf: true, icon: { type: WorkflowNodeDropdwonItemIconType.Icon, - name: "Megaphone", + name: "NotifyNodeIcon", }, }, ]; diff --git a/ui/src/domain/workflowRun.ts b/ui/src/domain/workflowRun.ts index f1c11411..045acabf 100644 --- a/ui/src/domain/workflowRun.ts +++ b/ui/src/domain/workflowRun.ts @@ -1,5 +1,3 @@ -import { type WorkflowOutput } from "./workflow"; - export interface WorkflowRunModel extends BaseModel { workflow: string; log: WorkflowRunLog[]; @@ -10,5 +8,12 @@ export interface WorkflowRunModel extends BaseModel { export type WorkflowRunLog = { nodeName: string; error: string; - outputs: WorkflowOutput[]; + outputs: WorkflowRunLogOutput[]; +}; + +export type WorkflowRunLogOutput = { + time: ISO8601String; + title: string; + content: string; + error: string; }; diff --git a/ui/src/global.d.ts b/ui/src/global.d.ts index 07951477..9c4f16f7 100644 --- a/ui/src/global.d.ts +++ b/ui/src/global.d.ts @@ -1,6 +1,8 @@ import { type BaseModel as PbBaseModel } from "pocketbase"; declare global { + declare type ISO8601String = string; + declare interface BaseModel extends PbBaseModel { created: ISO8601String; updated: ISO8601String; @@ -10,8 +12,6 @@ declare global { declare type MaybeModelRecord = T | Omit; declare type MaybeModelRecordWithId = T | Pick; - - declare type ISO8601String = string; } export {}; diff --git a/ui/src/i18n/index.ts b/ui/src/i18n/index.ts index fc212fbc..096453c3 100644 --- a/ui/src/i18n/index.ts +++ b/ui/src/i18n/index.ts @@ -3,7 +3,7 @@ import { initReactI18next } from "react-i18next"; import i18n from "i18next"; import LanguageDetector from "i18next-browser-languagedetector"; -import resources, { LOCALE_ZH_NAME, LOCALE_EN_NAME } from "./locales"; +import resources, { LOCALE_EN_NAME, LOCALE_ZH_NAME } from "./locales"; i18n .use(LanguageDetector) diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index 1148b5a4..a99047cf 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -3,9 +3,8 @@ "common.button.cancel": "Cancel", "common.button.copy": "Copy", "common.button.delete": "Delete", - "common.button.disable": "Disable", "common.button.edit": "Edit", - "common.button.enable": "Enable", + "common.button.more": "More", "common.button.ok": "Ok", "common.button.reset": "Reset", "common.button.save": "Save", diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index 0a60e7ba..1b1aec8e 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -7,14 +7,9 @@ "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 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.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": "Enable", "workflow.action.enable.failed.uncompleted": "Please complete the orchestration and publish the changes first", + "workflow.action.disable": "Disable", "workflow.props.name": "Name", "workflow.props.description": "Description", @@ -28,14 +23,28 @@ "workflow.props.created_at": "Created at", "workflow.props.updated_at": "Updated at", - "workflow.detail.orchestration.tab": "Orchestration", - "workflow.detail.runs.tab": "History runs", + "workflow.new.title": "Create Workflow", + "workflow.new.subtitle": "Apply, deploy and notify with Workflows", + "workflow.new.templates.title": "Choose a Workflow Template", + "workflow.new.templates.template.standard.title": "Standard template", + "workflow.new.templates.template.standard.description": "A standard operating procedure that includes application, deployment, and notification steps.", + "workflow.new.templates.template.blank.title": "Blank template", + "workflow.new.templates.template.blank.description": "Customize all the contents of the workflow from the beginning.", "workflow.detail.baseinfo.modal.title": "Workflow base information", "workflow.detail.baseinfo.form.name.label": "Name", "workflow.detail.baseinfo.form.name.placeholder": "Please enter name", "workflow.detail.baseinfo.form.description.label": "Description", "workflow.detail.baseinfo.form.description.placeholder": "Please enter description", + "workflow.detail.orchestration.tab": "Orchestration", + "workflow.detail.orchestration.action.discard": "Discard changes", + "workflow.detail.orchestration.action.discard.confirm": "Are you sure to discard your changes?", + "workflow.detail.orchestration.action.release": "Release", + "workflow.detail.orchestration.action.release.confirm": "Are you sure to release your changes?", + "workflow.detail.orchestration.action.release.failed.uncompleted": "Please complete the orchestration first", + "workflow.detail.orchestration.action.run": "Run", + "workflow.detail.orchestration.action.run.confirm": "There are unreleased changes, are you sure to run this workflow based on the latest released version?", + "workflow.detail.runs.tab": "History runs", "workflow.common.certificate.label": "Certificate", "workflow.node.setting.label": "Setting Node", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index 8d3fc779..468c61b3 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -3,9 +3,8 @@ "common.button.cancel": "取消", "common.button.copy": "复制", "common.button.delete": "刪除", - "common.button.disable": "禁用", "common.button.edit": "编辑", - "common.button.enable": "启用", + "common.button.more": "更多", "common.button.ok": "确定", "common.button.reset": "重置", "common.button.save": "保存", diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index 78ed5373..831876b2 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -7,14 +7,9 @@ "workflow.action.edit": "编辑工作流", "workflow.action.delete": "删除工作流", "workflow.action.delete.confirm": "确定要删除此工作流吗?", - "workflow.action.discard": "撤销更改", - "workflow.action.discard.confirm": "确定要撤销更改并回退到最近一次发布的版本吗?", - "workflow.action.release": "发布更改", - "workflow.action.release.confirm": "确定要发布更改吗?", - "workflow.action.release.failed.uncompleted": "请先完成流程编排", - "workflow.action.run": "执行", - "workflow.action.run.confirm": "存在未发布的更改,确定要按最近一次发布的版本来执行此工作流吗?", + "workflow.action.enable": "启用", "workflow.action.enable.failed.uncompleted": "请先完成流程编排并发布更改", + "workflow.action.disable": "禁用", "workflow.props.name": "名称", "workflow.props.description": "描述", @@ -28,14 +23,28 @@ "workflow.props.created_at": "创建时间", "workflow.props.updated_at": "更新时间", - "workflow.detail.orchestration.tab": "流程编排", - "workflow.detail.runs.tab": "执行历史", + "workflow.new.title": "新建工作流", + "workflow.new.subtitle": "使用工作流来申请证书、部署上传和发送通知", + "workflow.new.templates.title": "选择工作流模板", + "workflow.new.templates.template.standard.title": "标准模板", + "workflow.new.templates.template.standard.description": "一个包含申请 + 部署 + 通知步骤的标准工作流程。", + "workflow.new.templates.template.blank.title": "空白模板", + "workflow.new.templates.template.blank.description": "从零开始自定义工作流的任务内容。", "workflow.detail.baseinfo.modal.title": "编辑基本信息", "workflow.detail.baseinfo.form.name.label": "名称", "workflow.detail.baseinfo.form.name.placeholder": "请输入工作流名称", "workflow.detail.baseinfo.form.description.label": "描述", "workflow.detail.baseinfo.form.description.placeholder": "请输入工作流描述", + "workflow.detail.orchestration.tab": "流程编排", + "workflow.detail.orchestration.action.discard": "撤销更改", + "workflow.detail.orchestration.action.discard.confirm": "确定要撤销更改并回退到最近一次发布的版本吗?", + "workflow.detail.orchestration.action.release": "发布更改", + "workflow.detail.orchestration.action.release.confirm": "确定要发布更改吗?", + "workflow.detail.orchestration.action.release.failed.uncompleted": "流程编排未完成,请检查是否有节点未设置", + "workflow.detail.orchestration.action.run": "执行", + "workflow.detail.orchestration.action.run.confirm": "此工作流存在未发布的更改,将以最近一次发布的版本为准,确定要继续执行吗?", + "workflow.detail.runs.tab": "执行历史", "workflow.common.certificate.label": "证书", "workflow.node.setting.label": "设置节点", diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx index 677ed8cb..d94de3a0 100644 --- a/ui/src/pages/workflows/WorkflowDetail.tsx +++ b/ui/src/pages/workflows/WorkflowDetail.tsx @@ -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) => { @@ -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={[ - - {t("common.button.edit")}} onFinish={handleBaseInfoFormFinish} /> + {t("common.button.edit")}} onFinish={handleBaseInfoFormFinish} />, - + , - , - onClick: () => { - handleDeleteClick(); - }, + , + onClick: () => { + handleDeleteClick(); }, - ], - }} - trigger={["click"]} - > - + , ]} > {workflow.description} @@ -239,13 +257,13 @@ const WorkflowDetail = () => {
- { { key: "discard", disabled: !allowDiscard, - label: t("workflow.action.discard"), + label: t("workflow.detail.orchestration.action.discard"), icon: , onClick: handleDiscardClick, }, diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index 8feb4fbb..81ce12ee 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -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; } diff --git a/ui/src/pages/workflows/WorkflowNew.tsx b/ui/src/pages/workflows/WorkflowNew.tsx new file mode 100644 index 00000000..8cbe3713 --- /dev/null +++ b/ui/src/pages/workflows/WorkflowNew.tsx @@ -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(); + + 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 ( +
+ {NotificationContextHolder} + + + + {t("workflow.new.subtitle")} + + + +
+
+ +
{t("workflow.new.templates.title")}
+
+ + + + } + hoverable + onClick={() => handleTemplateSelect(TEMPLATE_KEY_STANDARD)} + > +
+ + +
+
+ + + } + hoverable + onClick={() => handleTemplateSelect(TEMPLATE_KEY_BLANK)} + > +
+ + +
+
+ +
+
+
+
+ ); +}; + +export default WorkflowNew; diff --git a/ui/src/stores/workflow/index.ts b/ui/src/stores/workflow/index.ts index 710dd06f..57b22188 100644 --- a/ui/src/stores/workflow/index.ts +++ b/ui/src/stores/workflow/index.ts @@ -1,18 +1,16 @@ import { create } from "zustand"; import { + type WorkflowBranchNode, + type WorkflowModel, + type WorkflowNode, addBranch, addNode, getExecuteMethod, getWorkflowOutputBeforeId, - initWorkflow, removeBranch, removeNode, updateNode, - type WorkflowBranchNode, - type WorkflowModel, - type WorkflowNode, - WorkflowNodeType, } from "@/domain/workflow"; import { get as getWorkflow, save as saveWorkflow } from "@/repository/workflow"; @@ -27,44 +25,29 @@ export type WorkflowState = { getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[]; switchEnable(): void; save(): void; - init(id?: string): void; + init(id: string): void; setBaseInfo: (name: string, description: string) => void; }; export const useWorkflowStore = create((set, get) => ({ - workflow: { - id: "", - name: "", - type: WorkflowNodeType.Start, - } as WorkflowModel, + workflow: {} as WorkflowModel, initialized: false, - init: async (id?: string) => { - let data = { - id: "", - name: "", - type: "auto", - } as WorkflowModel; - if (!id) { - data = initWorkflow(); - } else { - data = await getWorkflow(id); - } + init: async (id: string) => { + const data = await getWorkflow(id); set({ workflow: data, initialized: true, }); }, + setBaseInfo: async (name: string, description: string) => { const data: Record = { id: (get().workflow.id as string) ?? "", name: name || "", description: description || "", }; - if (!data.id) { - data.draft = get().workflow.draft as WorkflowNode; - } const resp = await saveWorkflow(data); set((state: WorkflowState) => { return { @@ -77,17 +60,18 @@ export const useWorkflowStore = create((set, get) => ({ }; }); }, + switchEnable: async () => { - const root = get().workflow.draft as WorkflowNode; + const root = get().workflow.content as WorkflowNode; const executeMethod = getExecuteMethod(root); const resp = await saveWorkflow({ id: (get().workflow.id as string) ?? "", content: root, enabled: !get().workflow.enabled, - hasDraft: false, type: executeMethod.type, crontab: executeMethod.crontab, }); + set((state: WorkflowState) => { return { workflow: { @@ -95,13 +79,13 @@ export const useWorkflowStore = create((set, get) => ({ id: resp.id, content: resp.content, enabled: resp.enabled, - hasDraft: false, type: resp.type, crontab: resp.crontab, }, }; }); }, + save: async () => { const root = get().workflow.draft as WorkflowNode; const executeMethod = getExecuteMethod(root); @@ -112,6 +96,7 @@ export const useWorkflowStore = create((set, get) => ({ type: executeMethod.type, crontab: executeMethod.crontab, }); + set((state: WorkflowState) => { return { workflow: { @@ -125,6 +110,7 @@ export const useWorkflowStore = create((set, get) => ({ }; }); }, + updateNode: async (node: WorkflowNode | WorkflowBranchNode) => { const newRoot = updateNode(get().workflow.draft as WorkflowNode, node); const resp = await saveWorkflow({ @@ -132,6 +118,7 @@ export const useWorkflowStore = create((set, get) => ({ draft: newRoot, hasDraft: true, }); + set((state: WorkflowState) => { return { workflow: { @@ -143,6 +130,7 @@ export const useWorkflowStore = create((set, get) => ({ }; }); }, + addNode: async (node: WorkflowNode | WorkflowBranchNode, preId: string) => { const newRoot = addNode(get().workflow.draft as WorkflowNode, preId, node); const resp = await saveWorkflow({ @@ -150,6 +138,7 @@ export const useWorkflowStore = create((set, get) => ({ draft: newRoot, hasDraft: true, }); + set((state: WorkflowState) => { return { workflow: { @@ -161,6 +150,7 @@ export const useWorkflowStore = create((set, get) => ({ }; }); }, + addBranch: async (branchId: string) => { const newRoot = addBranch(get().workflow.draft as WorkflowNode, branchId); const resp = await saveWorkflow({ @@ -168,6 +158,7 @@ export const useWorkflowStore = create((set, get) => ({ draft: newRoot, hasDraft: true, }); + set((state: WorkflowState) => { return { workflow: { @@ -179,6 +170,7 @@ export const useWorkflowStore = create((set, get) => ({ }; }); }, + removeBranch: async (branchId: string, index: number) => { const newRoot = removeBranch(get().workflow.draft as WorkflowNode, branchId, index); const resp = await saveWorkflow({ @@ -186,6 +178,7 @@ export const useWorkflowStore = create((set, get) => ({ draft: newRoot, hasDraft: true, }); + set((state: WorkflowState) => { return { workflow: { @@ -197,6 +190,7 @@ export const useWorkflowStore = create((set, get) => ({ }; }); }, + removeNode: async (nodeId: string) => { const newRoot = removeNode(get().workflow.draft as WorkflowNode, nodeId); const resp = await saveWorkflow({ @@ -204,6 +198,7 @@ export const useWorkflowStore = create((set, get) => ({ draft: newRoot, hasDraft: true, }); + set((state: WorkflowState) => { return { workflow: { @@ -215,6 +210,7 @@ export const useWorkflowStore = create((set, get) => ({ }; }); }, + getWorkflowOuptutBeforeId: (id: string, type: string) => { return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type); },