mirror of
https://github.com/woodchen-ink/certimate.git
synced 2025-07-18 09:21:56 +08:00
feat(ui): WorkflowNew page
This commit is contained in:
parent
b6dd2248c8
commit
c6a8f923e4
@ -37,8 +37,8 @@ type WorkflowNode struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Next *WorkflowNode `json:"next"`
|
Next *WorkflowNode `json:"next"`
|
||||||
Config map[string]any `json:"config"`
|
Config map[string]any `json:"config"`
|
||||||
Input []WorkflowNodeIo `json:"input"`
|
Input []WorkflowNodeIO `json:"input"`
|
||||||
Output []WorkflowNodeIo `json:"output"`
|
Output []WorkflowNodeIO `json:"output"`
|
||||||
|
|
||||||
Validated bool `json:"validated"`
|
Validated bool `json:"validated"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@ -76,16 +76,16 @@ func (n *WorkflowNode) GetConfigInt64(key string) int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkflowNodeIo struct {
|
type WorkflowNodeIO struct {
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Required bool `json:"required"`
|
Required bool `json:"required"`
|
||||||
Value any `json:"value"`
|
Value any `json:"value"`
|
||||||
ValueSelector WorkflowNodeIoValueSelector `json:"valueSelector"`
|
ValueSelector WorkflowNodeIOValueSelector `json:"valueSelector"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkflowNodeIoValueSelector struct {
|
type WorkflowNodeIOValueSelector struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,6 @@ type WorkflowOutput struct {
|
|||||||
Workflow string `json:"workflow"`
|
Workflow string `json:"workflow"`
|
||||||
NodeId string `json:"nodeId"`
|
NodeId string `json:"nodeId"`
|
||||||
Node *WorkflowNode `json:"node"`
|
Node *WorkflowNode `json:"node"`
|
||||||
Output []WorkflowNodeIo `json:"output"`
|
Output []WorkflowNodeIO `json:"output"`
|
||||||
Succeed bool `json:"succeed"`
|
Succeed bool `json:"succeed"`
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ func (w *WorkflowOutputRepository) Get(ctx context.Context, nodeId string) (*dom
|
|||||||
return nil, errors.New("failed to unmarshal node")
|
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 {
|
if err := record.UnmarshalJSONField("output", &output); err != nil {
|
||||||
return nil, errors.New("failed to unmarshal output")
|
return nil, errors.New("failed to unmarshal output")
|
||||||
}
|
}
|
||||||
|
BIN
ui/public/imgs/workflow/tpl-blank.png
Normal file
BIN
ui/public/imgs/workflow/tpl-blank.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
BIN
ui/public/imgs/workflow/tpl-standard.png
Normal file
BIN
ui/public/imgs/workflow/tpl-standard.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
@ -1,5 +1,5 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { BookOutlined as BookOutlinedIcon } from "@ant-design/icons";
|
import { ReadOutlined as ReadOutlinedIcon } from "@ant-design/icons";
|
||||||
import { Divider, Space, Typography } from "antd";
|
import { Divider, Space, Typography } from "antd";
|
||||||
|
|
||||||
import { version } from "@/domain/version";
|
import { version } from "@/domain/version";
|
||||||
@ -16,7 +16,7 @@ const Version = ({ className, style }: VersionProps) => {
|
|||||||
<Space className={className} style={style} size={4}>
|
<Space className={className} style={style} size={4}>
|
||||||
<Typography.Link type="secondary" href="https://docs.certimate.me" target="_blank">
|
<Typography.Link type="secondary" href="https://docs.certimate.me" target="_blank">
|
||||||
<div className="flex items-center justify-center space-x-1">
|
<div className="flex items-center justify-center space-x-1">
|
||||||
<BookOutlinedIcon />
|
<ReadOutlinedIcon />
|
||||||
<span>{t("common.menu.document")}</span>
|
<span>{t("common.menu.document")}</span>
|
||||||
</div>
|
</div>
|
||||||
</Typography.Link>
|
</Typography.Link>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { PlusOutlined as PlusOutlinedIcon } from "@ant-design/icons";
|
import { PlusOutlined as PlusOutlinedIcon } from "@ant-design/icons";
|
||||||
import { Dropdown } from "antd";
|
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 { useZustandShallowSelector } from "@/hooks";
|
||||||
import { useWorkflowStore } from "@/stores/workflow";
|
import { useWorkflowStore } from "@/stores/workflow";
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ const AddNode = ({ data }: NodeProps | BrandNodeProps) => {
|
|||||||
const { addNode } = useWorkflowStore(useZustandShallowSelector(["addNode"]));
|
const { addNode } = useWorkflowStore(useZustandShallowSelector(["addNode"]));
|
||||||
|
|
||||||
const handleTypeSelected = (type: WorkflowNodeType, provider?: string) => {
|
const handleTypeSelected = (type: WorkflowNodeType, provider?: string) => {
|
||||||
const node = newWorkflowNode(type, {
|
const node = newNode(type, {
|
||||||
providerType: provider,
|
providerType: provider,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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";
|
import { type WorkflowNodeDropdwonItemIcon, WorkflowNodeDropdwonItemIconType } from "@/domain/workflow";
|
||||||
|
|
||||||
const icons = new Map([
|
const icons = new Map([
|
||||||
["NotebookPen", <NotebookPen size={16} />],
|
["ApplyNodeIcon", <SolutionOutlinedIcon />],
|
||||||
["CloudUpload", <CloudUpload size={16} />],
|
["DeployNodeIcon", <CloudUploadOutlinedIcon />],
|
||||||
["GitFork", <GitFork size={16} />],
|
["BranchNodeIcon", <SisternodeOutlinedIcon />],
|
||||||
["Megaphone", <Megaphone size={16} />],
|
["NotifyNodeIcon", <SendOutlinedIcon />],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const DropdownMenuItemIcon = ({ type, name }: WorkflowNodeDropdwonItemIcon) => {
|
const DropdownMenuItemIcon = ({ type, name }: WorkflowNodeDropdwonItemIcon) => {
|
||||||
@ -13,7 +20,7 @@ const DropdownMenuItemIcon = ({ type, name }: WorkflowNodeDropdwonItemIcon) => {
|
|||||||
if (type === WorkflowNodeDropdwonItemIconType.Icon) {
|
if (type === WorkflowNodeDropdwonItemIconType.Icon) {
|
||||||
return icons.get(name);
|
return icons.get(name);
|
||||||
} else {
|
} else {
|
||||||
return <img src={name} className="inline-block size-4" />;
|
return <Avatar src={name} size="small" />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ export interface CertificateModel extends BaseModel {
|
|||||||
certUrl: string;
|
certUrl: string;
|
||||||
certStableUrl: string;
|
certStableUrl: string;
|
||||||
output: string;
|
output: string;
|
||||||
expireAt: string;
|
expireAt: ISO8601String;
|
||||||
workflow: string;
|
workflow: string;
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
expand: {
|
expand: {
|
||||||
|
@ -1,16 +1,10 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
import { produce } from "immer";
|
import { produce } from "immer";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import i18n from "@/i18n";
|
import i18n from "@/i18n";
|
||||||
import { deployProvidersMap } from "./provider";
|
import { deployProvidersMap } from "./provider";
|
||||||
|
|
||||||
export type WorkflowOutput = {
|
|
||||||
time: string;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface WorkflowModel extends BaseModel {
|
export interface WorkflowModel extends BaseModel {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@ -22,6 +16,7 @@ export interface WorkflowModel extends BaseModel {
|
|||||||
hasDraft?: boolean;
|
hasDraft?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #region Node
|
||||||
export enum WorkflowNodeType {
|
export enum WorkflowNodeType {
|
||||||
Start = "start",
|
Start = "start",
|
||||||
End = "end",
|
End = "end",
|
||||||
@ -33,7 +28,7 @@ export enum WorkflowNodeType {
|
|||||||
Custom = "custom",
|
Custom = "custom",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const workflowNodeTypeDefaultName: Map<WorkflowNodeType, string> = new Map([
|
const workflowNodeTypeDefaultNames: Map<WorkflowNodeType, string> = new Map([
|
||||||
[WorkflowNodeType.Start, i18n.t("workflow_node.start.label")],
|
[WorkflowNodeType.Start, i18n.t("workflow_node.start.label")],
|
||||||
[WorkflowNodeType.End, i18n.t("workflow_node.end.label")],
|
[WorkflowNodeType.End, i18n.t("workflow_node.end.label")],
|
||||||
[WorkflowNodeType.Branch, i18n.t("workflow_node.branch.label")],
|
[WorkflowNodeType.Branch, i18n.t("workflow_node.branch.label")],
|
||||||
@ -44,21 +39,7 @@ export const workflowNodeTypeDefaultName: Map<WorkflowNodeType, string> = new Ma
|
|||||||
[WorkflowNodeType.Custom, i18n.t("workflow_node.custom.title")],
|
[WorkflowNodeType.Custom, i18n.t("workflow_node.custom.title")],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type WorkflowNodeIo = {
|
const workflowNodeTypeDefaultInputs: Map<WorkflowNodeType, WorkflowNodeIO[]> = new Map([
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
required: boolean;
|
|
||||||
label: string;
|
|
||||||
value?: string;
|
|
||||||
valueSelector?: WorkflowNodeIoValueSelector;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowNodeIoValueSelector = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const workflowNodeTypeDefaultInput: Map<WorkflowNodeType, WorkflowNodeIo[]> = new Map([
|
|
||||||
[WorkflowNodeType.Apply, []],
|
[WorkflowNodeType.Apply, []],
|
||||||
[
|
[
|
||||||
WorkflowNodeType.Deploy,
|
WorkflowNodeType.Deploy,
|
||||||
@ -74,7 +55,7 @@ export const workflowNodeTypeDefaultInput: Map<WorkflowNodeType, WorkflowNodeIo[
|
|||||||
[WorkflowNodeType.Notify, []],
|
[WorkflowNodeType.Notify, []],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const workflowNodeTypeDefaultOutput: Map<WorkflowNodeType, WorkflowNodeIo[]> = new Map([
|
const workflowNodeTypeDefaultOutputs: Map<WorkflowNodeType, WorkflowNodeIO[]> = new Map([
|
||||||
[
|
[
|
||||||
WorkflowNodeType.Apply,
|
WorkflowNodeType.Apply,
|
||||||
[
|
[
|
||||||
@ -90,88 +71,122 @@ export const workflowNodeTypeDefaultOutput: Map<WorkflowNodeType, WorkflowNodeIo
|
|||||||
[WorkflowNodeType.Notify, []],
|
[WorkflowNodeType.Notify, []],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type WorkflowNodeConfig = Record<string, unknown>;
|
|
||||||
|
|
||||||
export type WorkflowNode = {
|
export type WorkflowNode = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: WorkflowNodeType;
|
type: WorkflowNodeType;
|
||||||
validated?: boolean;
|
|
||||||
|
|
||||||
input?: WorkflowNodeIo[];
|
config?: Record<string, unknown>;
|
||||||
config?: WorkflowNodeConfig;
|
input?: WorkflowNodeIO[];
|
||||||
output?: 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;
|
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;
|
branchIndex?: number;
|
||||||
providerType?: string;
|
providerType?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initWorkflow = (): WorkflowModel => {
|
export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions): WorkflowNode | WorkflowBranchNode => {
|
||||||
// 开始节点
|
const nodeTypeName = workflowNodeTypeDefaultNames.get(nodeType) || "";
|
||||||
const rs = newWorkflowNode(WorkflowNodeType.Start, {});
|
const nodeName = options.branchIndex != null ? `${nodeTypeName} ${options.branchIndex + 1}` : nodeTypeName;
|
||||||
let root = rs;
|
|
||||||
|
|
||||||
// 申请节点
|
const node: WorkflowNode | WorkflowBranchNode = {
|
||||||
root.next = newWorkflowNode(WorkflowNodeType.Apply, {});
|
id: nanoid(),
|
||||||
root = root.next;
|
name: nodeName,
|
||||||
|
type: nodeType,
|
||||||
// 部署节点
|
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (type === WorkflowNodeType.Apply || type === WorkflowNodeType.Deploy) {
|
switch (nodeType) {
|
||||||
rs = {
|
case WorkflowNodeType.Apply:
|
||||||
...rs,
|
case WorkflowNodeType.Deploy:
|
||||||
config: {
|
{
|
||||||
providerType: options.providerType,
|
node.config = {
|
||||||
},
|
providerType: options.providerType,
|
||||||
input: workflowNodeTypeDefaultInput.get(type),
|
};
|
||||||
output: workflowNodeTypeDefaultOutput.get(type),
|
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) {
|
return node;
|
||||||
rs.validated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === WorkflowNodeType.Branch) {
|
|
||||||
rs = {
|
|
||||||
...rs,
|
|
||||||
branches: [newWorkflowNode(WorkflowNodeType.Condition, { branchIndex: 0 }), newWorkflowNode(WorkflowNodeType.Condition, { branchIndex: 1 })],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return rs;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isWorkflowBranchNode = (node: WorkflowNode | WorkflowBranchNode): node is WorkflowBranchNode => {
|
export const isWorkflowBranchNode = (node: WorkflowNode | WorkflowBranchNode): node is WorkflowBranchNode => {
|
||||||
@ -226,7 +241,7 @@ export const addBranch = (node: WorkflowNode | WorkflowBranchNode, branchNodeId:
|
|||||||
return draft;
|
return draft;
|
||||||
}
|
}
|
||||||
current.branches.push(
|
current.branches.push(
|
||||||
newWorkflowNode(WorkflowNodeType.Condition, {
|
newNode(WorkflowNodeType.Condition, {
|
||||||
branchIndex: current.branches.length,
|
branchIndex: current.branches.length,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -340,21 +355,24 @@ export const getWorkflowOutputBeforeId = (node: WorkflowNode | WorkflowBranchNod
|
|||||||
return output;
|
return output;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAllNodesValidated = (node: WorkflowNode | WorkflowBranchNode): boolean => {
|
export const isAllNodesValidated = (node: WorkflowNode): boolean => {
|
||||||
let current = node as typeof node | undefined;
|
let current = node as typeof node | undefined;
|
||||||
while (current) {
|
while (current) {
|
||||||
if (!isWorkflowBranchNode(current) && !current.validated) {
|
if (current.type === WorkflowNodeType.Branch) {
|
||||||
return false;
|
for (const branch of current.branches!) {
|
||||||
}
|
|
||||||
if (isWorkflowBranchNode(current)) {
|
|
||||||
for (const branch of current.branches) {
|
|
||||||
if (!isAllNodesValidated(branch)) {
|
if (!isAllNodesValidated(branch)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (!current.validated) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
current = current.next;
|
current = current.next;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -372,14 +390,9 @@ export const getExecuteMethod = (node: WorkflowNode): { type: string; crontab: s
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowBranchNode = {
|
/**
|
||||||
id: string;
|
* @deprecated
|
||||||
name: string;
|
*/
|
||||||
type: WorkflowNodeType;
|
|
||||||
branches: WorkflowNode[];
|
|
||||||
next?: WorkflowNode | WorkflowBranchNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
type WorkflowNodeDropdwonItem = {
|
type WorkflowNodeDropdwonItem = {
|
||||||
type: WorkflowNodeType;
|
type: WorkflowNodeType;
|
||||||
providerType?: string;
|
providerType?: string;
|
||||||
@ -389,16 +402,25 @@ type WorkflowNodeDropdwonItem = {
|
|||||||
children?: WorkflowNodeDropdwonItem[];
|
children?: WorkflowNodeDropdwonItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
export enum WorkflowNodeDropdwonItemIconType {
|
export enum WorkflowNodeDropdwonItemIconType {
|
||||||
Icon,
|
Icon,
|
||||||
Provider,
|
Provider,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
export type WorkflowNodeDropdwonItemIcon = {
|
export type WorkflowNodeDropdwonItemIcon = {
|
||||||
type: WorkflowNodeDropdwonItemIconType;
|
type: WorkflowNodeDropdwonItemIconType;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
const workflowNodeDropdownDeployList: WorkflowNodeDropdwonItem[] = Array.from(deployProvidersMap.values()).map((item) => {
|
const workflowNodeDropdownDeployList: WorkflowNodeDropdwonItem[] = Array.from(deployProvidersMap.values()).map((item) => {
|
||||||
return {
|
return {
|
||||||
type: WorkflowNodeType.Apply,
|
type: WorkflowNodeType.Apply,
|
||||||
@ -412,41 +434,44 @@ const workflowNodeDropdownDeployList: WorkflowNodeDropdwonItem[] = Array.from(de
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
export const workflowNodeDropdownList: WorkflowNodeDropdwonItem[] = [
|
export const workflowNodeDropdownList: WorkflowNodeDropdwonItem[] = [
|
||||||
{
|
{
|
||||||
type: WorkflowNodeType.Apply,
|
type: WorkflowNodeType.Apply,
|
||||||
name: workflowNodeTypeDefaultName.get(WorkflowNodeType.Apply) ?? "",
|
name: workflowNodeTypeDefaultNames.get(WorkflowNodeType.Apply) ?? "",
|
||||||
icon: {
|
icon: {
|
||||||
type: WorkflowNodeDropdwonItemIconType.Icon,
|
type: WorkflowNodeDropdwonItemIconType.Icon,
|
||||||
name: "NotebookPen",
|
name: "ApplyNodeIcon",
|
||||||
},
|
},
|
||||||
leaf: true,
|
leaf: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: WorkflowNodeType.Deploy,
|
type: WorkflowNodeType.Deploy,
|
||||||
name: workflowNodeTypeDefaultName.get(WorkflowNodeType.Deploy) ?? "",
|
name: workflowNodeTypeDefaultNames.get(WorkflowNodeType.Deploy) ?? "",
|
||||||
icon: {
|
icon: {
|
||||||
type: WorkflowNodeDropdwonItemIconType.Icon,
|
type: WorkflowNodeDropdwonItemIconType.Icon,
|
||||||
name: "CloudUpload",
|
name: "DeployNodeIcon",
|
||||||
},
|
},
|
||||||
children: workflowNodeDropdownDeployList,
|
children: workflowNodeDropdownDeployList,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: WorkflowNodeType.Branch,
|
type: WorkflowNodeType.Branch,
|
||||||
name: workflowNodeTypeDefaultName.get(WorkflowNodeType.Branch) ?? "",
|
name: workflowNodeTypeDefaultNames.get(WorkflowNodeType.Branch) ?? "",
|
||||||
leaf: true,
|
leaf: true,
|
||||||
icon: {
|
icon: {
|
||||||
type: WorkflowNodeDropdwonItemIconType.Icon,
|
type: WorkflowNodeDropdwonItemIconType.Icon,
|
||||||
name: "GitFork",
|
name: "BranchNodeIcon",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: WorkflowNodeType.Notify,
|
type: WorkflowNodeType.Notify,
|
||||||
name: workflowNodeTypeDefaultName.get(WorkflowNodeType.Notify) ?? "",
|
name: workflowNodeTypeDefaultNames.get(WorkflowNodeType.Notify) ?? "",
|
||||||
leaf: true,
|
leaf: true,
|
||||||
icon: {
|
icon: {
|
||||||
type: WorkflowNodeDropdwonItemIconType.Icon,
|
type: WorkflowNodeDropdwonItemIconType.Icon,
|
||||||
name: "Megaphone",
|
name: "NotifyNodeIcon",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { type WorkflowOutput } from "./workflow";
|
|
||||||
|
|
||||||
export interface WorkflowRunModel extends BaseModel {
|
export interface WorkflowRunModel extends BaseModel {
|
||||||
workflow: string;
|
workflow: string;
|
||||||
log: WorkflowRunLog[];
|
log: WorkflowRunLog[];
|
||||||
@ -10,5 +8,12 @@ export interface WorkflowRunModel extends BaseModel {
|
|||||||
export type WorkflowRunLog = {
|
export type WorkflowRunLog = {
|
||||||
nodeName: string;
|
nodeName: string;
|
||||||
error: string;
|
error: string;
|
||||||
outputs: WorkflowOutput[];
|
outputs: WorkflowRunLogOutput[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkflowRunLogOutput = {
|
||||||
|
time: ISO8601String;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
error: string;
|
||||||
};
|
};
|
||||||
|
4
ui/src/global.d.ts
vendored
4
ui/src/global.d.ts
vendored
@ -1,6 +1,8 @@
|
|||||||
import { type BaseModel as PbBaseModel } from "pocketbase";
|
import { type BaseModel as PbBaseModel } from "pocketbase";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
declare type ISO8601String = string;
|
||||||
|
|
||||||
declare interface BaseModel extends PbBaseModel {
|
declare interface BaseModel extends PbBaseModel {
|
||||||
created: ISO8601String;
|
created: ISO8601String;
|
||||||
updated: ISO8601String;
|
updated: ISO8601String;
|
||||||
@ -10,8 +12,6 @@ declare global {
|
|||||||
declare type MaybeModelRecord<T extends BaseModel = BaseModel> = T | Omit<T, "id" | "created" | "updated" | "deleted">;
|
declare type MaybeModelRecord<T extends BaseModel = BaseModel> = T | Omit<T, "id" | "created" | "updated" | "deleted">;
|
||||||
|
|
||||||
declare type MaybeModelRecordWithId<T extends BaseModel = BaseModel> = T | Pick<T, "id">;
|
declare type MaybeModelRecordWithId<T extends BaseModel = BaseModel> = T | Pick<T, "id">;
|
||||||
|
|
||||||
declare type ISO8601String = string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
@ -3,7 +3,7 @@ import { initReactI18next } from "react-i18next";
|
|||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
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
|
i18n
|
||||||
.use(LanguageDetector)
|
.use(LanguageDetector)
|
||||||
|
@ -3,9 +3,8 @@
|
|||||||
"common.button.cancel": "Cancel",
|
"common.button.cancel": "Cancel",
|
||||||
"common.button.copy": "Copy",
|
"common.button.copy": "Copy",
|
||||||
"common.button.delete": "Delete",
|
"common.button.delete": "Delete",
|
||||||
"common.button.disable": "Disable",
|
|
||||||
"common.button.edit": "Edit",
|
"common.button.edit": "Edit",
|
||||||
"common.button.enable": "Enable",
|
"common.button.more": "More",
|
||||||
"common.button.ok": "Ok",
|
"common.button.ok": "Ok",
|
||||||
"common.button.reset": "Reset",
|
"common.button.reset": "Reset",
|
||||||
"common.button.save": "Save",
|
"common.button.save": "Save",
|
||||||
|
@ -7,14 +7,9 @@
|
|||||||
"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 changes",
|
"workflow.action.enable": "Enable",
|
||||||
"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.failed.uncompleted": "Please complete the orchestration and publish the changes first",
|
"workflow.action.enable.failed.uncompleted": "Please complete the orchestration and publish the changes first",
|
||||||
|
"workflow.action.disable": "Disable",
|
||||||
|
|
||||||
"workflow.props.name": "Name",
|
"workflow.props.name": "Name",
|
||||||
"workflow.props.description": "Description",
|
"workflow.props.description": "Description",
|
||||||
@ -28,14 +23,28 @@
|
|||||||
"workflow.props.created_at": "Created at",
|
"workflow.props.created_at": "Created at",
|
||||||
"workflow.props.updated_at": "Updated at",
|
"workflow.props.updated_at": "Updated at",
|
||||||
|
|
||||||
"workflow.detail.orchestration.tab": "Orchestration",
|
"workflow.new.title": "Create Workflow",
|
||||||
"workflow.detail.runs.tab": "History runs",
|
"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.modal.title": "Workflow base information",
|
||||||
"workflow.detail.baseinfo.form.name.label": "Name",
|
"workflow.detail.baseinfo.form.name.label": "Name",
|
||||||
"workflow.detail.baseinfo.form.name.placeholder": "Please enter name",
|
"workflow.detail.baseinfo.form.name.placeholder": "Please enter name",
|
||||||
"workflow.detail.baseinfo.form.description.label": "Description",
|
"workflow.detail.baseinfo.form.description.label": "Description",
|
||||||
"workflow.detail.baseinfo.form.description.placeholder": "Please enter 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.common.certificate.label": "Certificate",
|
||||||
"workflow.node.setting.label": "Setting Node",
|
"workflow.node.setting.label": "Setting Node",
|
||||||
|
@ -3,9 +3,8 @@
|
|||||||
"common.button.cancel": "取消",
|
"common.button.cancel": "取消",
|
||||||
"common.button.copy": "复制",
|
"common.button.copy": "复制",
|
||||||
"common.button.delete": "刪除",
|
"common.button.delete": "刪除",
|
||||||
"common.button.disable": "禁用",
|
|
||||||
"common.button.edit": "编辑",
|
"common.button.edit": "编辑",
|
||||||
"common.button.enable": "启用",
|
"common.button.more": "更多",
|
||||||
"common.button.ok": "确定",
|
"common.button.ok": "确定",
|
||||||
"common.button.reset": "重置",
|
"common.button.reset": "重置",
|
||||||
"common.button.save": "保存",
|
"common.button.save": "保存",
|
||||||
|
@ -7,14 +7,9 @@
|
|||||||
"workflow.action.edit": "编辑工作流",
|
"workflow.action.edit": "编辑工作流",
|
||||||
"workflow.action.delete": "删除工作流",
|
"workflow.action.delete": "删除工作流",
|
||||||
"workflow.action.delete.confirm": "确定要删除此工作流吗?",
|
"workflow.action.delete.confirm": "确定要删除此工作流吗?",
|
||||||
"workflow.action.discard": "撤销更改",
|
"workflow.action.enable": "启用",
|
||||||
"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.failed.uncompleted": "请先完成流程编排并发布更改",
|
"workflow.action.enable.failed.uncompleted": "请先完成流程编排并发布更改",
|
||||||
|
"workflow.action.disable": "禁用",
|
||||||
|
|
||||||
"workflow.props.name": "名称",
|
"workflow.props.name": "名称",
|
||||||
"workflow.props.description": "描述",
|
"workflow.props.description": "描述",
|
||||||
@ -28,14 +23,28 @@
|
|||||||
"workflow.props.created_at": "创建时间",
|
"workflow.props.created_at": "创建时间",
|
||||||
"workflow.props.updated_at": "更新时间",
|
"workflow.props.updated_at": "更新时间",
|
||||||
|
|
||||||
"workflow.detail.orchestration.tab": "流程编排",
|
"workflow.new.title": "新建工作流",
|
||||||
"workflow.detail.runs.tab": "执行历史",
|
"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.modal.title": "编辑基本信息",
|
||||||
"workflow.detail.baseinfo.form.name.label": "名称",
|
"workflow.detail.baseinfo.form.name.label": "名称",
|
||||||
"workflow.detail.baseinfo.form.name.placeholder": "请输入工作流名称",
|
"workflow.detail.baseinfo.form.name.placeholder": "请输入工作流名称",
|
||||||
"workflow.detail.baseinfo.form.description.label": "描述",
|
"workflow.detail.baseinfo.form.description.label": "描述",
|
||||||
"workflow.detail.baseinfo.form.description.placeholder": "请输入工作流描述",
|
"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.common.certificate.label": "证书",
|
||||||
"workflow.node.setting.label": "设置节点",
|
"workflow.node.setting.label": "设置节点",
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
ApartmentOutlined as ApartmentOutlinedIcon,
|
ApartmentOutlined as ApartmentOutlinedIcon,
|
||||||
CaretRightOutlined as CaretRightOutlinedIcon,
|
CaretRightOutlined as CaretRightOutlinedIcon,
|
||||||
DeleteOutlined as DeleteOutlinedIcon,
|
DeleteOutlined as DeleteOutlinedIcon,
|
||||||
|
DownOutlined as DownOutlinedIcon,
|
||||||
EllipsisOutlined as EllipsisOutlinedIcon,
|
EllipsisOutlined as EllipsisOutlinedIcon,
|
||||||
HistoryOutlined as HistoryOutlinedIcon,
|
HistoryOutlined as HistoryOutlinedIcon,
|
||||||
UndoOutlined as UndoOutlinedIcon,
|
UndoOutlined as UndoOutlinedIcon,
|
||||||
@ -45,8 +46,8 @@ const WorkflowDetail = () => {
|
|||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TODO: loading
|
// TODO: loading
|
||||||
init(workflowId);
|
init(workflowId!);
|
||||||
}, [workflowId, init]);
|
}, [workflowId]);
|
||||||
|
|
||||||
const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration");
|
const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration");
|
||||||
|
|
||||||
@ -70,10 +71,13 @@ const WorkflowDetail = () => {
|
|||||||
|
|
||||||
const [allowDiscard, setAllowDiscard] = useState(false);
|
const [allowDiscard, setAllowDiscard] = useState(false);
|
||||||
const [allowRelease, setAllowRelease] = useState(false);
|
const [allowRelease, setAllowRelease] = useState(false);
|
||||||
|
const [allowRun, setAllowRun] = useState(false);
|
||||||
useDeepCompareEffect(() => {
|
useDeepCompareEffect(() => {
|
||||||
|
const hasReleased = !!workflow.content;
|
||||||
const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content);
|
const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content);
|
||||||
setAllowDiscard(hasChanges && !workflowRunning);
|
setAllowDiscard(!workflowRunning && hasReleased && hasChanges);
|
||||||
setAllowRelease(hasChanges && !workflowRunning);
|
setAllowRelease(!workflowRunning && hasChanges);
|
||||||
|
setAllowRun(hasReleased);
|
||||||
}, [workflow, workflowRunning]);
|
}, [workflow, workflowRunning]);
|
||||||
|
|
||||||
const handleBaseInfoFormFinish = async (values: Pick<WorkflowModel, "name" | "description">) => {
|
const handleBaseInfoFormFinish = async (values: Pick<WorkflowModel, "name" | "description">) => {
|
||||||
@ -86,13 +90,18 @@ const WorkflowDetail = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEnableChange = () => {
|
const handleEnableChange = async () => {
|
||||||
if (!workflow.enabled && !isAllNodesValidated(workflow.content!)) {
|
if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) {
|
||||||
messageApi.warning(t("workflow.action.enable.failed.uncompleted"));
|
messageApi.warning(t("workflow.action.enable.failed.uncompleted"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switchEnable();
|
try {
|
||||||
|
await switchEnable();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteClick = () => {
|
const handleDeleteClick = () => {
|
||||||
@ -114,18 +123,24 @@ const WorkflowDetail = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDiscardClick = () => {
|
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 = () => {
|
const handleReleaseClick = () => {
|
||||||
if (!isAllNodesValidated(workflow.draft!)) {
|
if (!isAllNodesValidated(workflow.draft!)) {
|
||||||
messageApi.warning(t("workflow.action.release.failed.uncompleted"));
|
messageApi.warning(t("workflow.detail.orchestration.action.release.failed.uncompleted"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
modalApi.confirm({
|
modalApi.confirm({
|
||||||
title: t("workflow.action.release"),
|
title: t("workflow.detail.orchestration.action.release"),
|
||||||
content: t("workflow.action.release.confirm"),
|
content: t("workflow.detail.orchestration.action.release.confirm"),
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
try {
|
try {
|
||||||
await save();
|
await save();
|
||||||
@ -148,8 +163,8 @@ const WorkflowDetail = () => {
|
|||||||
const { promise, resolve, reject } = Promise.withResolvers();
|
const { promise, resolve, reject } = Promise.withResolvers();
|
||||||
if (workflow.hasDraft) {
|
if (workflow.hasDraft) {
|
||||||
modalApi.confirm({
|
modalApi.confirm({
|
||||||
title: t("workflow.action.run"),
|
title: t("workflow.detail.orchestration.action.run"),
|
||||||
content: t("workflow.action.run.confirm"),
|
content: t("workflow.detail.orchestration.action.run.confirm"),
|
||||||
onOk: () => resolve(void 0),
|
onOk: () => resolve(void 0),
|
||||||
onCancel: () => reject(),
|
onCancel: () => reject(),
|
||||||
});
|
});
|
||||||
@ -164,7 +179,7 @@ const WorkflowDetail = () => {
|
|||||||
try {
|
try {
|
||||||
await runWorkflow(workflowId!);
|
await runWorkflow(workflowId!);
|
||||||
|
|
||||||
messageApi.warning(t("common.text.operation_succeeded"));
|
messageApi.success(t("common.text.operation_succeeded"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ClientResponseError && err.isAbort) {
|
if (err instanceof ClientResponseError && err.isAbort) {
|
||||||
return;
|
return;
|
||||||
@ -189,30 +204,33 @@ const WorkflowDetail = () => {
|
|||||||
style={{ paddingBottom: 0 }}
|
style={{ paddingBottom: 0 }}
|
||||||
title={workflow.name}
|
title={workflow.name}
|
||||||
extra={[
|
extra={[
|
||||||
<Button.Group key="actions">
|
<WorkflowBaseInfoModalForm key="edit" data={workflow} trigger={<Button>{t("common.button.edit")}</Button>} onFinish={handleBaseInfoFormFinish} />,
|
||||||
<WorkflowBaseInfoModalForm data={workflow} trigger={<Button>{t("common.button.edit")}</Button>} onFinish={handleBaseInfoFormFinish} />
|
|
||||||
|
|
||||||
<Button onClick={handleEnableChange}>{workflow.enabled ? t("common.button.disable") : t("common.button.enable")}</Button>
|
<Button key="enable" onClick={handleEnableChange}>
|
||||||
|
{workflow.enabled ? t("workflow.action.disable") : t("workflow.action.enable")}
|
||||||
|
</Button>,
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menu={{
|
key="more"
|
||||||
items: [
|
menu={{
|
||||||
{
|
items: [
|
||||||
key: "delete",
|
{
|
||||||
label: t("common.button.delete"),
|
key: "delete",
|
||||||
danger: true,
|
label: t("workflow.action.delete"),
|
||||||
icon: <DeleteOutlinedIcon />,
|
danger: true,
|
||||||
onClick: () => {
|
icon: <DeleteOutlinedIcon />,
|
||||||
handleDeleteClick();
|
onClick: () => {
|
||||||
},
|
handleDeleteClick();
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
}}
|
],
|
||||||
trigger={["click"]}
|
}}
|
||||||
>
|
trigger={["click"]}
|
||||||
<Button icon={<EllipsisOutlinedIcon />} />
|
>
|
||||||
</Dropdown>
|
<Button icon={<DownOutlinedIcon />} iconPosition="end">
|
||||||
</Button.Group>,
|
{t("common.button.more")}
|
||||||
|
</Button>
|
||||||
|
</Dropdown>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Typography.Paragraph type="secondary">{workflow.description}</Typography.Paragraph>
|
<Typography.Paragraph type="secondary">{workflow.description}</Typography.Paragraph>
|
||||||
@ -239,13 +257,13 @@ const WorkflowDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="absolute top-0 right-0 z-[1]">
|
<div className="absolute top-0 right-0 z-[1]">
|
||||||
<Space>
|
<Space>
|
||||||
<Button icon={<CaretRightOutlinedIcon />} loading={workflowRunning} type="primary" onClick={handleRunClick}>
|
<Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={workflowRunning} type="primary" onClick={handleRunClick}>
|
||||||
{t("workflow.action.run")}
|
{t("workflow.detail.orchestration.action.run")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button.Group>
|
<Button.Group>
|
||||||
<Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}>
|
<Button color="primary" disabled={!allowRelease} variant="outlined" onClick={handleReleaseClick}>
|
||||||
{t("workflow.action.release")}
|
{t("workflow.detail.orchestration.action.release")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@ -254,7 +272,7 @@ const WorkflowDetail = () => {
|
|||||||
{
|
{
|
||||||
key: "discard",
|
key: "discard",
|
||||||
disabled: !allowDiscard,
|
disabled: !allowDiscard,
|
||||||
label: t("workflow.action.discard"),
|
label: t("workflow.detail.orchestration.action.discard"),
|
||||||
icon: <UndoOutlinedIcon />,
|
icon: <UndoOutlinedIcon />,
|
||||||
onClick: handleDiscardClick,
|
onClick: handleDiscardClick,
|
||||||
},
|
},
|
||||||
|
@ -245,7 +245,7 @@ const WorkflowList = () => {
|
|||||||
|
|
||||||
const handleEnabledChange = async (workflow: WorkflowModel) => {
|
const handleEnabledChange = async (workflow: WorkflowModel) => {
|
||||||
try {
|
try {
|
||||||
if (!workflow.enabled && !isAllNodesValidated(workflow.content!)) {
|
if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) {
|
||||||
messageApi.warning(t("workflow.action.enable.failed.uncompleted"));
|
messageApi.warning(t("workflow.action.enable.failed.uncompleted"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
123
ui/src/pages/workflows/WorkflowNew.tsx
Normal file
123
ui/src/pages/workflows/WorkflowNew.tsx
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { PageHeader } from "@ant-design/pro-components";
|
||||||
|
import { Card, Col, Row, Spin, Typography, notification } from "antd";
|
||||||
|
import { sleep } from "radash";
|
||||||
|
|
||||||
|
import { type WorkflowModel, initWorkflow } from "@/domain/workflow";
|
||||||
|
import { save as saveWorkflow } from "@/repository/workflow";
|
||||||
|
import { getErrMsg } from "@/utils/error";
|
||||||
|
|
||||||
|
const TEMPLATE_KEY_BLANK = "blank" as const;
|
||||||
|
const TEMPLATE_KEY_STANDARD = "standard" as const;
|
||||||
|
type TemplateKeys = typeof TEMPLATE_KEY_BLANK | typeof TEMPLATE_KEY_STANDARD;
|
||||||
|
|
||||||
|
const WorkflowNew = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||||
|
|
||||||
|
const templateGridSpans = {
|
||||||
|
xs: { flex: "100%" },
|
||||||
|
md: { flex: "100%" },
|
||||||
|
lg: { flex: "50%" },
|
||||||
|
xl: { flex: "50%" },
|
||||||
|
xxl: { flex: "50%" },
|
||||||
|
};
|
||||||
|
const [templateSelectKey, setTemplateSelectKey] = useState<TemplateKeys>();
|
||||||
|
|
||||||
|
const handleTemplateSelect = async (key: TemplateKeys) => {
|
||||||
|
if (templateSelectKey) return;
|
||||||
|
|
||||||
|
setTemplateSelectKey(key);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let workflow: WorkflowModel;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case TEMPLATE_KEY_BLANK:
|
||||||
|
workflow = initWorkflow();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TEMPLATE_KEY_STANDARD:
|
||||||
|
workflow = initWorkflow({ template: "standard" });
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw "Invalid args: `key`";
|
||||||
|
}
|
||||||
|
|
||||||
|
workflow = await saveWorkflow(workflow);
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
await navigate(`/workflows/${workflow.id}`, { replace: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||||
|
|
||||||
|
setTemplateSelectKey(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{NotificationContextHolder}
|
||||||
|
|
||||||
|
<Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }}>
|
||||||
|
<PageHeader title={t("workflow.new.title")}>
|
||||||
|
<Typography.Paragraph type="secondary">{t("workflow.new.subtitle")}</Typography.Paragraph>
|
||||||
|
</PageHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="max-w-[960px] mx-auto px-2">
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
<div className="mt-4 mb-8 text-xl">{t("workflow.new.templates.title")}</div>
|
||||||
|
</Typography.Text>
|
||||||
|
|
||||||
|
<Row className="justify-stretch" gutter={[16, 16]}>
|
||||||
|
<Col {...templateGridSpans}>
|
||||||
|
<Card
|
||||||
|
className="size-full"
|
||||||
|
cover={<img className="min-h-[120px] object-contain" src="/imgs/workflow/tpl-standard.png" />}
|
||||||
|
hoverable
|
||||||
|
onClick={() => handleTemplateSelect(TEMPLATE_KEY_STANDARD)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 w-full">
|
||||||
|
<Card.Meta
|
||||||
|
className="flex-grow"
|
||||||
|
title={t("workflow.new.templates.template.standard.title")}
|
||||||
|
description={t("workflow.new.templates.template.standard.description")}
|
||||||
|
/>
|
||||||
|
<Spin spinning={templateSelectKey === TEMPLATE_KEY_STANDARD} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col {...templateGridSpans}>
|
||||||
|
<Card
|
||||||
|
className="size-full"
|
||||||
|
cover={<img className="min-h-[120px] object-contain" src="/imgs/workflow/tpl-blank.png" />}
|
||||||
|
hoverable
|
||||||
|
onClick={() => handleTemplateSelect(TEMPLATE_KEY_BLANK)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 w-full">
|
||||||
|
<Card.Meta
|
||||||
|
className="flex-grow"
|
||||||
|
title={t("workflow.new.templates.template.blank.title")}
|
||||||
|
description={t("workflow.new.templates.template.blank.description")}
|
||||||
|
/>
|
||||||
|
<Spin spinning={templateSelectKey === TEMPLATE_KEY_BLANK} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkflowNew;
|
@ -1,18 +1,16 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type WorkflowBranchNode,
|
||||||
|
type WorkflowModel,
|
||||||
|
type WorkflowNode,
|
||||||
addBranch,
|
addBranch,
|
||||||
addNode,
|
addNode,
|
||||||
getExecuteMethod,
|
getExecuteMethod,
|
||||||
getWorkflowOutputBeforeId,
|
getWorkflowOutputBeforeId,
|
||||||
initWorkflow,
|
|
||||||
removeBranch,
|
removeBranch,
|
||||||
removeNode,
|
removeNode,
|
||||||
updateNode,
|
updateNode,
|
||||||
type WorkflowBranchNode,
|
|
||||||
type WorkflowModel,
|
|
||||||
type WorkflowNode,
|
|
||||||
WorkflowNodeType,
|
|
||||||
} from "@/domain/workflow";
|
} from "@/domain/workflow";
|
||||||
import { get as getWorkflow, save as saveWorkflow } from "@/repository/workflow";
|
import { get as getWorkflow, save as saveWorkflow } from "@/repository/workflow";
|
||||||
|
|
||||||
@ -27,44 +25,29 @@ export type WorkflowState = {
|
|||||||
getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[];
|
getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[];
|
||||||
switchEnable(): void;
|
switchEnable(): void;
|
||||||
save(): void;
|
save(): void;
|
||||||
init(id?: string): void;
|
init(id: string): void;
|
||||||
setBaseInfo: (name: string, description: string) => void;
|
setBaseInfo: (name: string, description: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||||
workflow: {
|
workflow: {} as WorkflowModel,
|
||||||
id: "",
|
|
||||||
name: "",
|
|
||||||
type: WorkflowNodeType.Start,
|
|
||||||
} as WorkflowModel,
|
|
||||||
initialized: false,
|
initialized: false,
|
||||||
init: async (id?: string) => {
|
|
||||||
let data = {
|
|
||||||
id: "",
|
|
||||||
name: "",
|
|
||||||
type: "auto",
|
|
||||||
} as WorkflowModel;
|
|
||||||
|
|
||||||
if (!id) {
|
init: async (id: string) => {
|
||||||
data = initWorkflow();
|
const data = await getWorkflow(id);
|
||||||
} else {
|
|
||||||
data = await getWorkflow(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
set({
|
set({
|
||||||
workflow: data,
|
workflow: data,
|
||||||
initialized: true,
|
initialized: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setBaseInfo: async (name: string, description: string) => {
|
setBaseInfo: async (name: string, description: string) => {
|
||||||
const data: Record<string, string | boolean | WorkflowNode> = {
|
const data: Record<string, string | boolean | WorkflowNode> = {
|
||||||
id: (get().workflow.id as string) ?? "",
|
id: (get().workflow.id as string) ?? "",
|
||||||
name: name || "",
|
name: name || "",
|
||||||
description: description || "",
|
description: description || "",
|
||||||
};
|
};
|
||||||
if (!data.id) {
|
|
||||||
data.draft = get().workflow.draft as WorkflowNode;
|
|
||||||
}
|
|
||||||
const resp = await saveWorkflow(data);
|
const resp = await saveWorkflow(data);
|
||||||
set((state: WorkflowState) => {
|
set((state: WorkflowState) => {
|
||||||
return {
|
return {
|
||||||
@ -77,17 +60,18 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
switchEnable: async () => {
|
switchEnable: async () => {
|
||||||
const root = get().workflow.draft as WorkflowNode;
|
const root = get().workflow.content as WorkflowNode;
|
||||||
const executeMethod = getExecuteMethod(root);
|
const executeMethod = getExecuteMethod(root);
|
||||||
const resp = await saveWorkflow({
|
const resp = await saveWorkflow({
|
||||||
id: (get().workflow.id as string) ?? "",
|
id: (get().workflow.id as string) ?? "",
|
||||||
content: root,
|
content: root,
|
||||||
enabled: !get().workflow.enabled,
|
enabled: !get().workflow.enabled,
|
||||||
hasDraft: false,
|
|
||||||
type: executeMethod.type,
|
type: executeMethod.type,
|
||||||
crontab: executeMethod.crontab,
|
crontab: executeMethod.crontab,
|
||||||
});
|
});
|
||||||
|
|
||||||
set((state: WorkflowState) => {
|
set((state: WorkflowState) => {
|
||||||
return {
|
return {
|
||||||
workflow: {
|
workflow: {
|
||||||
@ -95,13 +79,13 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
id: resp.id,
|
id: resp.id,
|
||||||
content: resp.content,
|
content: resp.content,
|
||||||
enabled: resp.enabled,
|
enabled: resp.enabled,
|
||||||
hasDraft: false,
|
|
||||||
type: resp.type,
|
type: resp.type,
|
||||||
crontab: resp.crontab,
|
crontab: resp.crontab,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
save: async () => {
|
save: async () => {
|
||||||
const root = get().workflow.draft as WorkflowNode;
|
const root = get().workflow.draft as WorkflowNode;
|
||||||
const executeMethod = getExecuteMethod(root);
|
const executeMethod = getExecuteMethod(root);
|
||||||
@ -112,6 +96,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
type: executeMethod.type,
|
type: executeMethod.type,
|
||||||
crontab: executeMethod.crontab,
|
crontab: executeMethod.crontab,
|
||||||
});
|
});
|
||||||
|
|
||||||
set((state: WorkflowState) => {
|
set((state: WorkflowState) => {
|
||||||
return {
|
return {
|
||||||
workflow: {
|
workflow: {
|
||||||
@ -125,6 +110,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updateNode: async (node: WorkflowNode | WorkflowBranchNode) => {
|
updateNode: async (node: WorkflowNode | WorkflowBranchNode) => {
|
||||||
const newRoot = updateNode(get().workflow.draft as WorkflowNode, node);
|
const newRoot = updateNode(get().workflow.draft as WorkflowNode, node);
|
||||||
const resp = await saveWorkflow({
|
const resp = await saveWorkflow({
|
||||||
@ -132,6 +118,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
draft: newRoot,
|
draft: newRoot,
|
||||||
hasDraft: true,
|
hasDraft: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
set((state: WorkflowState) => {
|
set((state: WorkflowState) => {
|
||||||
return {
|
return {
|
||||||
workflow: {
|
workflow: {
|
||||||
@ -143,6 +130,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
addNode: async (node: WorkflowNode | WorkflowBranchNode, preId: string) => {
|
addNode: async (node: WorkflowNode | WorkflowBranchNode, preId: string) => {
|
||||||
const newRoot = addNode(get().workflow.draft as WorkflowNode, preId, node);
|
const newRoot = addNode(get().workflow.draft as WorkflowNode, preId, node);
|
||||||
const resp = await saveWorkflow({
|
const resp = await saveWorkflow({
|
||||||
@ -150,6 +138,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
draft: newRoot,
|
draft: newRoot,
|
||||||
hasDraft: true,
|
hasDraft: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
set((state: WorkflowState) => {
|
set((state: WorkflowState) => {
|
||||||
return {
|
return {
|
||||||
workflow: {
|
workflow: {
|
||||||
@ -161,6 +150,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
addBranch: async (branchId: string) => {
|
addBranch: async (branchId: string) => {
|
||||||
const newRoot = addBranch(get().workflow.draft as WorkflowNode, branchId);
|
const newRoot = addBranch(get().workflow.draft as WorkflowNode, branchId);
|
||||||
const resp = await saveWorkflow({
|
const resp = await saveWorkflow({
|
||||||
@ -168,6 +158,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
draft: newRoot,
|
draft: newRoot,
|
||||||
hasDraft: true,
|
hasDraft: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
set((state: WorkflowState) => {
|
set((state: WorkflowState) => {
|
||||||
return {
|
return {
|
||||||
workflow: {
|
workflow: {
|
||||||
@ -179,6 +170,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
removeBranch: async (branchId: string, index: number) => {
|
removeBranch: async (branchId: string, index: number) => {
|
||||||
const newRoot = removeBranch(get().workflow.draft as WorkflowNode, branchId, index);
|
const newRoot = removeBranch(get().workflow.draft as WorkflowNode, branchId, index);
|
||||||
const resp = await saveWorkflow({
|
const resp = await saveWorkflow({
|
||||||
@ -186,6 +178,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
draft: newRoot,
|
draft: newRoot,
|
||||||
hasDraft: true,
|
hasDraft: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
set((state: WorkflowState) => {
|
set((state: WorkflowState) => {
|
||||||
return {
|
return {
|
||||||
workflow: {
|
workflow: {
|
||||||
@ -197,6 +190,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
removeNode: async (nodeId: string) => {
|
removeNode: async (nodeId: string) => {
|
||||||
const newRoot = removeNode(get().workflow.draft as WorkflowNode, nodeId);
|
const newRoot = removeNode(get().workflow.draft as WorkflowNode, nodeId);
|
||||||
const resp = await saveWorkflow({
|
const resp = await saveWorkflow({
|
||||||
@ -204,6 +198,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
draft: newRoot,
|
draft: newRoot,
|
||||||
hasDraft: true,
|
hasDraft: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
set((state: WorkflowState) => {
|
set((state: WorkflowState) => {
|
||||||
return {
|
return {
|
||||||
workflow: {
|
workflow: {
|
||||||
@ -215,6 +210,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getWorkflowOuptutBeforeId: (id: string, type: string) => {
|
getWorkflowOuptutBeforeId: (id: string, type: string) => {
|
||||||
return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type);
|
return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type);
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user