mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-09-11 22:09:44 +00:00
Feat: Merge branch sprint web flow (#1854)
# Description Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. # How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration # Snapshots: Include snapshots for easier review. # Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have already rebased the commits and make the commit message conform to the project standard. - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] Any dependent changes have been merged and published in downstream modules
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
import dataclasses
|
import dataclasses
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
@@ -24,6 +25,7 @@ from .storage import (
|
|||||||
StorageItem,
|
StorageItem,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
_SCHEMA = "dbgpt-fs"
|
_SCHEMA = "dbgpt-fs"
|
||||||
|
|
||||||
|
|
||||||
@@ -133,6 +135,14 @@ class FileStorageURI:
|
|||||||
self.version = version
|
self.version = version
|
||||||
self.custom_params = custom_params or {}
|
self.custom_params = custom_params or {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_local_file(cls, uri: str) -> bool:
|
||||||
|
"""Check if the URI is local."""
|
||||||
|
parsed = urlparse(uri)
|
||||||
|
if not parsed.scheme or parsed.scheme == "file":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse(cls, uri: str) -> "FileStorageURI":
|
def parse(cls, uri: str) -> "FileStorageURI":
|
||||||
"""Parse the URI string."""
|
"""Parse the URI string."""
|
||||||
@@ -313,6 +323,13 @@ class FileStorageSystem:
|
|||||||
file_size = file_data.tell() # Get the file size
|
file_size = file_data.tell() # Get the file size
|
||||||
file_data.seek(0) # Reset file pointer
|
file_data.seek(0) # Reset file pointer
|
||||||
|
|
||||||
|
# filter None value
|
||||||
|
custom_metadata = (
|
||||||
|
{k: v for k, v in custom_metadata.items() if v is not None}
|
||||||
|
if custom_metadata
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
with root_tracer.start_span(
|
with root_tracer.start_span(
|
||||||
"file_storage_system.save_file.calculate_hash",
|
"file_storage_system.save_file.calculate_hash",
|
||||||
):
|
):
|
||||||
@@ -329,7 +346,7 @@ class FileStorageSystem:
|
|||||||
storage_type=storage_type,
|
storage_type=storage_type,
|
||||||
storage_path=storage_path,
|
storage_path=storage_path,
|
||||||
uri=str(uri),
|
uri=str(uri),
|
||||||
custom_metadata=custom_metadata or {},
|
custom_metadata=custom_metadata,
|
||||||
file_hash=file_hash,
|
file_hash=file_hash,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -339,6 +356,25 @@ class FileStorageSystem:
|
|||||||
@trace("file_storage_system.get_file")
|
@trace("file_storage_system.get_file")
|
||||||
def get_file(self, uri: str) -> Tuple[BinaryIO, FileMetadata]:
|
def get_file(self, uri: str) -> Tuple[BinaryIO, FileMetadata]:
|
||||||
"""Get the file data from the storage backend."""
|
"""Get the file data from the storage backend."""
|
||||||
|
if FileStorageURI.is_local_file(uri):
|
||||||
|
local_file_name = uri.split("/")[-1]
|
||||||
|
if not os.path.exists(uri):
|
||||||
|
raise FileNotFoundError(f"File not found: {uri}")
|
||||||
|
|
||||||
|
dummy_metadata = FileMetadata(
|
||||||
|
file_id=local_file_name,
|
||||||
|
bucket="dummy_bucket",
|
||||||
|
file_name=local_file_name,
|
||||||
|
file_size=-1,
|
||||||
|
storage_type="local",
|
||||||
|
storage_path=uri,
|
||||||
|
uri=uri,
|
||||||
|
custom_metadata={},
|
||||||
|
file_hash="",
|
||||||
|
)
|
||||||
|
logger.info(f"Reading local file: {uri}")
|
||||||
|
return open(uri, "rb"), dummy_metadata # noqa: SIM115
|
||||||
|
|
||||||
parsed_uri = FileStorageURI.parse(uri)
|
parsed_uri = FileStorageURI.parse(uri)
|
||||||
metadata = self.metadata_storage.load(
|
metadata = self.metadata_storage.load(
|
||||||
FileMetadataIdentifier(
|
FileMetadataIdentifier(
|
||||||
|
@@ -117,7 +117,9 @@ async def test_auth():
|
|||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/flows", response_model=Result[None], dependencies=[Depends(check_api_key)]
|
"/flows",
|
||||||
|
response_model=Result[ServerResponse],
|
||||||
|
dependencies=[Depends(check_api_key)],
|
||||||
)
|
)
|
||||||
async def create(
|
async def create(
|
||||||
request: ServeRequest, service: Service = Depends(get_service)
|
request: ServeRequest, service: Service = Depends(get_service)
|
||||||
|
@@ -1079,3 +1079,132 @@ class ExampleFlowTagsOperator(MapOperator[str, str]):
|
|||||||
async def map(self, user_name: str) -> str:
|
async def map(self, user_name: str) -> str:
|
||||||
"""Map the user name to the tags."""
|
"""Map the user name to the tags."""
|
||||||
return "Your name is %s, and your tags are %s." % (user_name, "higher-order")
|
return "Your name is %s, and your tags are %s." % (user_name, "higher-order")
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleFlowCodeEditorOperator(MapOperator[str, str]):
|
||||||
|
"""An example flow operator that includes a code editor as parameter."""
|
||||||
|
|
||||||
|
metadata = ViewMetadata(
|
||||||
|
label="Example Flow Code Editor",
|
||||||
|
name="example_flow_code_editor",
|
||||||
|
category=OperatorCategory.EXAMPLE,
|
||||||
|
description="An example flow operator that includes a code editor as parameter.",
|
||||||
|
parameters=[
|
||||||
|
Parameter.build_from(
|
||||||
|
"Code Editor",
|
||||||
|
"code",
|
||||||
|
type=str,
|
||||||
|
placeholder="Please input your code",
|
||||||
|
description="The code you want to edit.",
|
||||||
|
ui=ui.UICodeEditor(
|
||||||
|
language="python",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
inputs=[
|
||||||
|
IOField.build_from(
|
||||||
|
"User Name",
|
||||||
|
"user_name",
|
||||||
|
str,
|
||||||
|
description="The name of the user.",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IOField.build_from(
|
||||||
|
"Code",
|
||||||
|
"code",
|
||||||
|
str,
|
||||||
|
description="Result of the code.",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, code: str, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
async def map(self, user_name: str) -> str:
|
||||||
|
"""Map the user name to the code."""
|
||||||
|
from dbgpt.util.code_utils import UNKNOWN, extract_code
|
||||||
|
|
||||||
|
code = self.code
|
||||||
|
exitcode = -1
|
||||||
|
try:
|
||||||
|
code_blocks = extract_code(self.code)
|
||||||
|
if len(code_blocks) < 1:
|
||||||
|
logger.info(
|
||||||
|
f"No executable code found in: \n{code}",
|
||||||
|
)
|
||||||
|
raise ValueError(f"No executable code found in: \n{code}")
|
||||||
|
elif len(code_blocks) > 1 and code_blocks[0][0] == UNKNOWN:
|
||||||
|
# found code blocks, execute code and push "last_n_messages" back
|
||||||
|
logger.info(
|
||||||
|
f"Missing available code block type, unable to execute code,"
|
||||||
|
f"\n{code}",
|
||||||
|
)
|
||||||
|
raise ValueError(
|
||||||
|
"Missing available code block type, unable to execute code, "
|
||||||
|
f"\n{code}"
|
||||||
|
)
|
||||||
|
exitcode, logs = await self.blocking_func_to_async(
|
||||||
|
self.execute_code_blocks, code_blocks
|
||||||
|
)
|
||||||
|
# exitcode, logs = self.execute_code_blocks(code_blocks)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to execute code: {e}")
|
||||||
|
logs = f"Failed to execute code: {e}"
|
||||||
|
return (
|
||||||
|
f"Your name is {user_name}, and your code is \n\n```python\n{self.code}"
|
||||||
|
f"\n\n```\n\nThe execution result is \n\n```\n{logs}\n\n```\n\n"
|
||||||
|
f"Exit code: {exitcode}."
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute_code_blocks(self, code_blocks):
|
||||||
|
"""Execute the code blocks and return the result."""
|
||||||
|
from dbgpt.util.code_utils import execute_code, infer_lang
|
||||||
|
from dbgpt.util.utils import colored
|
||||||
|
|
||||||
|
logs_all = ""
|
||||||
|
exitcode = -1
|
||||||
|
_code_execution_config = {"use_docker": False}
|
||||||
|
for i, code_block in enumerate(code_blocks):
|
||||||
|
lang, code = code_block
|
||||||
|
if not lang:
|
||||||
|
lang = infer_lang(code)
|
||||||
|
print(
|
||||||
|
colored(
|
||||||
|
f"\n>>>>>>>> EXECUTING CODE BLOCK {i} "
|
||||||
|
f"(inferred language is {lang})...",
|
||||||
|
"red",
|
||||||
|
),
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
if lang in ["bash", "shell", "sh"]:
|
||||||
|
exitcode, logs, image = execute_code(
|
||||||
|
code, lang=lang, **_code_execution_config
|
||||||
|
)
|
||||||
|
elif lang in ["python", "Python"]:
|
||||||
|
if code.startswith("# filename: "):
|
||||||
|
filename = code[11 : code.find("\n")].strip()
|
||||||
|
else:
|
||||||
|
filename = None
|
||||||
|
exitcode, logs, image = execute_code(
|
||||||
|
code,
|
||||||
|
lang="python",
|
||||||
|
filename=filename,
|
||||||
|
**_code_execution_config,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# In case the language is not supported, we return an error message.
|
||||||
|
exitcode, logs, image = (
|
||||||
|
1,
|
||||||
|
f"unknown language {lang}",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
# raise NotImplementedError
|
||||||
|
if image is not None:
|
||||||
|
_code_execution_config["use_docker"] = image
|
||||||
|
logs_all += "\n" + logs
|
||||||
|
if exitcode != 0:
|
||||||
|
return exitcode, logs_all
|
||||||
|
return exitcode, logs_all
|
||||||
|
@@ -34,8 +34,18 @@ import {
|
|||||||
SpaceConfig,
|
SpaceConfig,
|
||||||
} from '@/types/knowledge';
|
} from '@/types/knowledge';
|
||||||
import { UpdatePromptParams, IPrompt, PromptParams } from '@/types/prompt';
|
import { UpdatePromptParams, IPrompt, PromptParams } from '@/types/prompt';
|
||||||
import { IFlow, IFlowNode, IFlowResponse, IFlowUpdateParam } from '@/types/flow';
|
import {
|
||||||
import { IAgent, IApp, IAppData, ITeamModal } from '@/types/app';
|
IFlow,
|
||||||
|
IFlowNode,
|
||||||
|
IFlowResponse,
|
||||||
|
IFlowUpdateParam,
|
||||||
|
IFlowRefreshParams,
|
||||||
|
IFlowExportParams,
|
||||||
|
IFlowImportParams,
|
||||||
|
IUploadFileRequestParams,
|
||||||
|
IUploadFileResponse,
|
||||||
|
} from '@/types/flow';
|
||||||
|
import { IAgent, IApp, IAppData } from '@/types/app';
|
||||||
|
|
||||||
/** App */
|
/** App */
|
||||||
export const postScenes = () => {
|
export const postScenes = () => {
|
||||||
@@ -262,27 +272,60 @@ export const addPrompt = (data: UpdatePromptParams) => {
|
|||||||
|
|
||||||
/** AWEL Flow */
|
/** AWEL Flow */
|
||||||
export const addFlow = (data: IFlowUpdateParam) => {
|
export const addFlow = (data: IFlowUpdateParam) => {
|
||||||
return POST<IFlowUpdateParam, IFlow>('/api/v1/serve/awel/flows', data);
|
return POST<IFlowUpdateParam, IFlow>('/api/v2/serve/awel/flows', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFlows = () => {
|
export const getFlows = () => {
|
||||||
return GET<null, IFlowResponse>('/api/v1/serve/awel/flows');
|
return GET<null, IFlowResponse>('/api/v2/serve/awel/flows');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFlowById = (id: string) => {
|
export const getFlowById = (id: string) => {
|
||||||
return GET<null, IFlow>(`/api/v1/serve/awel/flows/${id}`);
|
return GET<null, IFlow>(`/api/v2/serve/awel/flows/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateFlowById = (id: string, data: IFlowUpdateParam) => {
|
export const updateFlowById = (id: string, data: IFlowUpdateParam) => {
|
||||||
return PUT<IFlowUpdateParam, IFlow>(`/api/v1/serve/awel/flows/${id}`, data);
|
return PUT<IFlowUpdateParam, IFlow>(`/api/v2/serve/awel/flows/${id}`, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteFlowById = (id: string) => {
|
export const deleteFlowById = (id: string) => {
|
||||||
return DELETE<null, null>(`/api/v1/serve/awel/flows/${id}`);
|
return DELETE<null, null>(`/api/v2/serve/awel/flows/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFlowNodes = () => {
|
export const getFlowNodes = () => {
|
||||||
return GET<null, Array<IFlowNode>>(`/api/v1/serve/awel/nodes`);
|
return GET<null, Array<IFlowNode>>(`/api/v2/serve/awel/nodes`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refreshFlowNodeById = (data: IFlowRefreshParams) => {
|
||||||
|
return POST<IFlowRefreshParams, IFlowNode>('/api/v2/serve/awel/nodes/refresh', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: wait for interface update
|
||||||
|
export const debugFlow = (data: any) => {
|
||||||
|
return POST<any, IFlowNode>('/api/v2/serve/awel/flow/debug', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportFlow = (data: IFlowExportParams) => {
|
||||||
|
return GET<IFlowExportParams, any>('/api/v2/serve/awel/flow/export', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const importFlow = (data: IFlowImportParams) => {
|
||||||
|
return POST<IFlowImportParams, any>('/api/v2/serve/awel/flow/import', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFlowTemplateList = () => {
|
||||||
|
return GET<null, Array<any>>('/api/v2/serve/awel/flow/templates');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFlowTemplateById = (id: string) => {
|
||||||
|
return GET<null, any>(`/api/v2/serve/awel/flow/templates/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadFile = (data: IUploadFileRequestParams) => {
|
||||||
|
return POST<IUploadFileRequestParams, Array<IUploadFileResponse>>('/api/v2/serve/file/files/dbgpt', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const downloadFile = (fileId: string) => {
|
||||||
|
return GET<null, any>(`/api/v2/serve/file/files/dbgpt/${fileId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** app */
|
/** app */
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { IFlowNode, IFlowNodeParameter } from '@/types/flow';
|
import { IFlowNode, IFlowNodeParameter } from '@/types/flow';
|
||||||
|
import { refreshFlowNodeById, apiInterceptors } from '@/client/api';
|
||||||
import { Checkbox, Input, InputNumber, Select, Tooltip } from 'antd';
|
import { Checkbox, Input, InputNumber, Select, Tooltip } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import RequiredIcon from './required-icon';
|
import RequiredIcon from './required-icon';
|
||||||
@@ -34,27 +35,36 @@ const NodeParamHandler: React.FC<NodeParamHandlerProps> = ({ node, data, label,
|
|||||||
data.value = value;
|
data.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderLabelWithTooltip(data: IFlowNodeParameter) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{data.label}:<RequiredIcon optional={data.optional} />
|
||||||
|
{data.description && (
|
||||||
|
<Tooltip title={data.description}>
|
||||||
|
<InfoCircleOutlined className="ml-2 cursor-pointer" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// render node parameters based on AWEL1.0
|
// render node parameters based on AWEL1.0
|
||||||
function renderNodeWithoutUiParam(data: IFlowNodeParameter) {
|
function renderNodeWithoutUiParam(data: IFlowNodeParameter) {
|
||||||
let defaultValue = data.value ?? data.default;
|
let defaultValue = data.value ?? data.default;
|
||||||
|
|
||||||
|
console.log('datacc', data);
|
||||||
switch (data.type_name) {
|
switch (data.type_name) {
|
||||||
case 'int':
|
case 'int':
|
||||||
case 'float':
|
case 'float':
|
||||||
return (
|
return (
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<p>
|
{renderLabelWithTooltip(data)}
|
||||||
{data.label}:<RequiredIcon optional={data.optional} />
|
|
||||||
{data.description && (
|
|
||||||
<Tooltip title={data.description}>
|
|
||||||
<InfoCircleOutlined className="ml-2 cursor-pointer" />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<InputNumber
|
<InputNumber
|
||||||
className="w-full"
|
className="w-full"
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
onChange={(value: number | null) => {
|
onChange={(value: number | null) => {
|
||||||
|
console.log('value', value);
|
||||||
|
|
||||||
onChange(value);
|
onChange(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -63,14 +73,7 @@ const NodeParamHandler: React.FC<NodeParamHandlerProps> = ({ node, data, label,
|
|||||||
case 'str':
|
case 'str':
|
||||||
return (
|
return (
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<p>
|
{renderLabelWithTooltip(data)}
|
||||||
{data.label}:<RequiredIcon optional={data.optional} />
|
|
||||||
{data.description && (
|
|
||||||
<Tooltip title={data.description}>
|
|
||||||
<InfoCircleOutlined className="ml-2 cursor-pointer" />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
{data.options?.length > 0 ? (
|
{data.options?.length > 0 ? (
|
||||||
<Select
|
<Select
|
||||||
className="w-full nodrag"
|
className="w-full nodrag"
|
||||||
@@ -94,32 +97,47 @@ const NodeParamHandler: React.FC<NodeParamHandlerProps> = ({ node, data, label,
|
|||||||
defaultValue = defaultValue === 'True' ? true : defaultValue;
|
defaultValue = defaultValue === 'True' ? true : defaultValue;
|
||||||
return (
|
return (
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<p>
|
{renderLabelWithTooltip(data)}
|
||||||
{data.label}:<RequiredIcon optional={data.optional} />
|
<Checkbox
|
||||||
{data.description && (
|
className="ml-2"
|
||||||
<Tooltip title={data.description}>
|
defaultChecked={defaultValue}
|
||||||
<InfoCircleOutlined className="ml-2 cursor-pointer" />
|
onChange={(e) => {
|
||||||
</Tooltip>
|
onChange(e.target.checked);
|
||||||
)}
|
}}
|
||||||
<Checkbox
|
/>
|
||||||
className="ml-2"
|
|
||||||
defaultChecked={defaultValue}
|
|
||||||
onChange={(e) => {
|
|
||||||
onChange(e.target.checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// render node parameters based on AWEL2.0
|
// TODO: refresh flow node
|
||||||
function renderNodeWithUiParam(data: IFlowNodeParameter) {
|
async function refreshFlowNode() {
|
||||||
let defaultValue = data.value ?? data.default;
|
// setLoading(true);
|
||||||
const props = { data, defaultValue, onChange };
|
const params = {
|
||||||
|
id: '',
|
||||||
|
type_name: '',
|
||||||
|
type_cls: '',
|
||||||
|
flow_type: 'operator' as const,
|
||||||
|
refresh: [
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
depends: [
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
value: '',
|
||||||
|
has_value: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const [_, data] = await apiInterceptors(refreshFlowNodeById(params));
|
||||||
|
// setLoading(false);
|
||||||
|
// setFlowList(data?.items ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
switch (data?.ui?.ui_type) {
|
function renderComponentByType(type: string, props?: any) {
|
||||||
|
switch (type) {
|
||||||
case 'select':
|
case 'select':
|
||||||
return <RenderSelect {...props} />;
|
return <RenderSelect {...props} />;
|
||||||
case 'cascader':
|
case 'cascader':
|
||||||
@@ -144,8 +162,8 @@ const NodeParamHandler: React.FC<NodeParamHandlerProps> = ({ node, data, label,
|
|||||||
return <RenderPassword {...props} />;
|
return <RenderPassword {...props} />;
|
||||||
case 'upload':
|
case 'upload':
|
||||||
return <RenderUpload {...props} />;
|
return <RenderUpload {...props} />;
|
||||||
case 'variables':
|
case 'variables':
|
||||||
return <RenderVariables {...props} />;
|
return <RenderVariables {...props} />;
|
||||||
case 'code_editor':
|
case 'code_editor':
|
||||||
return <RenderCodeEditor {...props} />;
|
return <RenderCodeEditor {...props} />;
|
||||||
default:
|
default:
|
||||||
@@ -153,6 +171,21 @@ const NodeParamHandler: React.FC<NodeParamHandlerProps> = ({ node, data, label,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// render node parameters based on AWEL2.0
|
||||||
|
function renderNodeWithUiParam(data: IFlowNodeParameter) {
|
||||||
|
let defaultValue = data.value ?? data.default;
|
||||||
|
const props = { data, defaultValue, onChange };
|
||||||
|
|
||||||
|
console.log('xxx', props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{renderLabelWithTooltip(data)}
|
||||||
|
{renderComponentByType(data?.ui?.ui_type, props)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (data.category === 'resource') {
|
if (data.category === 'resource') {
|
||||||
return <NodeHandler node={node} data={data} type="target" label={label} index={index} />;
|
return <NodeHandler node={node} data={data} type="target" label={label} index={index} />;
|
||||||
} else if (data.category === 'common') {
|
} else if (data.category === 'common') {
|
||||||
|
@@ -22,19 +22,17 @@ export const RenderCodeEditor = (params: Props) => {
|
|||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOk = () => {
|
const onOk = () => {
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const onCancel = () => {
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* 设置弹窗宽度
|
|
||||||
*/
|
|
||||||
const modalWidth = useMemo(() => {
|
const modalWidth = useMemo(() => {
|
||||||
if (data?.ui?.editor?.width) {
|
if (data?.ui?.editor?.width) {
|
||||||
return data?.ui?.editor?.width + 100
|
return data?.ui?.editor?.width + 100;
|
||||||
}
|
}
|
||||||
return '80%';
|
return '80%';
|
||||||
}, [data?.ui?.editor?.width]);
|
}, [data?.ui?.editor?.width]);
|
||||||
@@ -44,16 +42,16 @@ export const RenderCodeEditor = (params: Props) => {
|
|||||||
<Button type="primary" onClick={showModal}>
|
<Button type="primary" onClick={showModal}>
|
||||||
{t('openCodeEditor')}
|
{t('openCodeEditor')}
|
||||||
</Button>
|
</Button>
|
||||||
<Modal title={t('openCodeEditor')} width={modalWidth} open={isModalOpen} onOk={handleOk} onCancel={handleCancel}>
|
|
||||||
|
<Modal title={t('openCodeEditor')} width={modalWidth} open={isModalOpen} onOk={onOk} onCancel={onCancel}>
|
||||||
<Editor
|
<Editor
|
||||||
{...data?.ui?.attr}
|
{...attr}
|
||||||
width={data?.ui?.editor?.width || '100%'}
|
width={data?.ui?.editor?.width || '100%'}
|
||||||
value={defaultValue}
|
value={defaultValue}
|
||||||
style={{ padding: '10px' }}
|
|
||||||
height={data?.ui?.editor?.height || 200}
|
height={data?.ui?.editor?.height || 200}
|
||||||
defaultLanguage={data?.ui?.language}
|
defaultLanguage={data?.ui?.language}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
theme='vs-dark'
|
theme="vs-dark"
|
||||||
options={{
|
options={{
|
||||||
minimap: {
|
minimap: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
@@ -11,7 +11,6 @@ type Props = {
|
|||||||
|
|
||||||
export const RenderDatePicker = (params: Props) => {
|
export const RenderDatePicker = (params: Props) => {
|
||||||
const { data, defaultValue, onChange } = params;
|
const { data, defaultValue, onChange } = params;
|
||||||
|
|
||||||
const attr = convertKeysToCamelCase(data.ui?.attr || {});
|
const attr = convertKeysToCamelCase(data.ui?.attr || {});
|
||||||
|
|
||||||
const onChangeDate: DatePickerProps['onChange'] = (date, dateString) => {
|
const onChangeDate: DatePickerProps['onChange'] = (date, dateString) => {
|
||||||
|
@@ -4,13 +4,13 @@ import { convertKeysToCamelCase } from '@/utils/flow';
|
|||||||
|
|
||||||
const { Password } = Input;
|
const { Password } = Input;
|
||||||
|
|
||||||
type TextAreaProps = {
|
type Props = {
|
||||||
data: IFlowNodeParameter;
|
data: IFlowNodeParameter;
|
||||||
defaultValue: any;
|
defaultValue: any;
|
||||||
onChange: (value: any) => void;
|
onChange: (value: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RenderPassword = (params: TextAreaProps) => {
|
export const RenderPassword = (params: Props) => {
|
||||||
const { data, defaultValue, onChange } = params;
|
const { data, defaultValue, onChange } = params;
|
||||||
const attr = convertKeysToCamelCase(data.ui?.attr || {});
|
const attr = convertKeysToCamelCase(data.ui?.attr || {});
|
||||||
|
|
||||||
|
@@ -4,13 +4,13 @@ import { Col, InputNumber, Row, Slider, Space } from 'antd';
|
|||||||
import type { InputNumberProps } from 'antd';
|
import type { InputNumberProps } from 'antd';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
type TextAreaProps = {
|
type Props = {
|
||||||
data: IFlowNodeParameter;
|
data: IFlowNodeParameter;
|
||||||
defaultValue: any;
|
defaultValue: any;
|
||||||
onChange: (value: any) => void;
|
onChange: (value: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RenderSlider = (params: TextAreaProps) => {
|
export const RenderSlider = (params: Props) => {
|
||||||
const { data, defaultValue, onChange } = params;
|
const { data, defaultValue, onChange } = params;
|
||||||
const attr = convertKeysToCamelCase(data.ui?.attr || {});
|
const attr = convertKeysToCamelCase(data.ui?.attr || {});
|
||||||
const [inputValue, setInputValue] = useState(defaultValue);
|
const [inputValue, setInputValue] = useState(defaultValue);
|
||||||
@@ -18,7 +18,6 @@ export const RenderSlider = (params: TextAreaProps) => {
|
|||||||
setInputValue(newValue);
|
setInputValue(newValue);
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
};
|
};
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@@ -5,13 +5,13 @@ import classNames from 'classnames';
|
|||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
type TextAreaProps = {
|
type Props = {
|
||||||
data: IFlowNodeParameter;
|
data: IFlowNodeParameter;
|
||||||
defaultValue: any;
|
defaultValue: any;
|
||||||
onChange: (value: any) => void;
|
onChange: (value: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RenderTextArea = (params: TextAreaProps) => {
|
export const RenderTextArea = (params: Props) => {
|
||||||
const { data, defaultValue, onChange } = params;
|
const { data, defaultValue, onChange } = params;
|
||||||
|
|
||||||
const attr = convertKeysToCamelCase(data.ui?.attr || {});
|
const attr = convertKeysToCamelCase(data.ui?.attr || {});
|
||||||
|
@@ -1,15 +1,16 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import type { TimePickerProps } from 'antd';
|
import type { TimePickerProps } from 'antd';
|
||||||
import { TimePicker } from 'antd';
|
import { TimePicker } from 'antd';
|
||||||
import { IFlowNodeParameter } from '@/types/flow';
|
import { IFlowNodeParameter } from '@/types/flow';
|
||||||
import { convertKeysToCamelCase } from '@/utils/flow';
|
import { convertKeysToCamelCase } from '@/utils/flow';
|
||||||
|
|
||||||
type TextAreaProps = {
|
type Props = {
|
||||||
data: IFlowNodeParameter;
|
data: IFlowNodeParameter;
|
||||||
defaultValue: any;
|
defaultValue: any;
|
||||||
onChange: (value: any) => void;
|
onChange: (value: any) => void;
|
||||||
};
|
};
|
||||||
export const RenderTimePicker = (params: TextAreaProps) => {
|
|
||||||
|
export const RenderTimePicker = (params: Props) => {
|
||||||
const { data, defaultValue, onChange } = params;
|
const { data, defaultValue, onChange } = params;
|
||||||
const attr = convertKeysToCamelCase(data.ui?.attr || {});
|
const attr = convertKeysToCamelCase(data.ui?.attr || {});
|
||||||
|
|
||||||
|
@@ -3,27 +3,24 @@ import { TreeSelect } from 'antd';
|
|||||||
import { IFlowNodeParameter } from '@/types/flow';
|
import { IFlowNodeParameter } from '@/types/flow';
|
||||||
import { convertKeysToCamelCase } from '@/utils/flow';
|
import { convertKeysToCamelCase } from '@/utils/flow';
|
||||||
|
|
||||||
type TextAreaProps = {
|
type Props = {
|
||||||
data: IFlowNodeParameter;
|
data: IFlowNodeParameter;
|
||||||
defaultValue: any;
|
defaultValue: any;
|
||||||
onChange: (value: any) => void;
|
onChange: (value: any) => void;
|
||||||
};
|
};
|
||||||
export const RenderTreeSelect = (params: TextAreaProps) => {
|
export const RenderTreeSelect = (params: Props) => {
|
||||||
const { data, defaultValue, onChange } = params;
|
const { data, defaultValue, onChange } = params;
|
||||||
const attr = convertKeysToCamelCase(data.ui?.attr || {});
|
const attr = convertKeysToCamelCase(data.ui?.attr || {});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-2 text-sm">
|
<TreeSelect
|
||||||
<TreeSelect
|
{...attr}
|
||||||
className="w-full nodrag"
|
className="w-full nodrag"
|
||||||
fieldNames={{ label: 'label', value: 'value', children: 'children' }}
|
fieldNames={{ label: 'label', value: 'value', children: 'children' }}
|
||||||
{...attr}
|
value={defaultValue}
|
||||||
style={{ width: '100%' }}
|
treeDefaultExpandAll
|
||||||
value={defaultValue}
|
onChange={onChange}
|
||||||
treeDefaultExpandAll
|
treeData={data.options}
|
||||||
onChange={onChange}
|
/>
|
||||||
treeData={data.options}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { UploadOutlined } from '@ant-design/icons';
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
import type { UploadProps } from 'antd';
|
import type { UploadProps } from 'antd';
|
||||||
import { Button, message, Upload } from 'antd';
|
import { Button, Upload } from 'antd';
|
||||||
import { convertKeysToCamelCase } from '@/utils/flow';
|
import { convertKeysToCamelCase } from '@/utils/flow';
|
||||||
import { IFlowNodeParameter } from '@/types/flow';
|
import { IFlowNodeParameter } from '@/types/flow';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -28,7 +28,7 @@ export const RenderUpload = (params: Props) => {
|
|||||||
const attr = convertKeysToCamelCase(data.ui?.attr || {});
|
const attr = convertKeysToCamelCase(data.ui?.attr || {});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ textAlign: 'center' }} className="p-2 text-sm">
|
<div className="p-2 text-sm text-center">
|
||||||
<Upload {...attr} {...props}>
|
<Upload {...attr} {...props}>
|
||||||
<Button icon={<UploadOutlined />}>{t('UploadData')}</Button>
|
<Button icon={<UploadOutlined />}>{t('UploadData')}</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
|
@@ -5,8 +5,8 @@ import ButtonEdge from '@/components/flow/button-edge';
|
|||||||
import CanvasNode from '@/components/flow/canvas-node';
|
import CanvasNode from '@/components/flow/canvas-node';
|
||||||
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
|
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
|
||||||
import { checkFlowDataRequied, getUniqueNodeId, mapHumpToUnderline, mapUnderlineToHump } from '@/utils/flow';
|
import { checkFlowDataRequied, getUniqueNodeId, mapHumpToUnderline, mapUnderlineToHump } from '@/utils/flow';
|
||||||
import { FrownOutlined, SaveOutlined } from '@ant-design/icons';
|
import { ExportOutlined, FrownOutlined, ImportOutlined, SaveOutlined } from '@ant-design/icons';
|
||||||
import { Button, Checkbox, Divider, Form, Input, Modal, Space, message, notification } from 'antd';
|
import { Button, Checkbox, Divider, Form, Input, Modal, Space, Tooltip, message, notification } from 'antd';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import React, { DragEvent, useCallback, useEffect, useRef, useState } from 'react';
|
import React, { DragEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -150,7 +150,7 @@ const Canvas: React.FC<Props> = () => {
|
|||||||
form.setFieldsValue({ name: result });
|
form.setFieldsValue({ name: result });
|
||||||
}
|
}
|
||||||
|
|
||||||
function clickSave() {
|
function onSave() {
|
||||||
const flowData = reactFlow.toObject() as IFlowData;
|
const flowData = reactFlow.toObject() as IFlowData;
|
||||||
const [check, node, message] = checkFlowDataRequied(flowData);
|
const [check, node, message] = checkFlowDataRequied(flowData);
|
||||||
if (!check && message) {
|
if (!check && message) {
|
||||||
@@ -175,10 +175,40 @@ const Canvas: React.FC<Props> = () => {
|
|||||||
setIsModalVisible(true);
|
setIsModalVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: EXport flow data
|
||||||
|
function onExport() {
|
||||||
|
const flowData = reactFlow.toObject() as IFlowData;
|
||||||
|
const blob = new Blob([JSON.stringify(flowData)], { type: 'text/plain;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'flow.json';
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Import flow data
|
||||||
|
function onImport() {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.json';
|
||||||
|
input.onchange = async (e: any) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (event) => {
|
||||||
|
const flowData = JSON.parse(event.target?.result as string) as IFlowData;
|
||||||
|
setNodes(flowData.nodes);
|
||||||
|
setEdges(flowData.edges);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
input.click;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSaveFlow() {
|
async function handleSaveFlow() {
|
||||||
const { name, label, description = '', editable = false, state = 'deployed' } = form.getFieldsValue();
|
const { name, label, description = '', editable = false, state = 'deployed' } = form.getFieldsValue();
|
||||||
console.log(form.getFieldsValue());
|
console.log(form.getFieldsValue());
|
||||||
const reactFlowObject = mapHumpToUnderline(reactFlow.toObject() as IFlowData);
|
const reactFlowObject = mapHumpToUnderline(reactFlow.toObject() as IFlowData);
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
const [, , res] = await apiInterceptors(updateFlowById(id, { name, label, description, editable, uid: id, flow_data: reactFlowObject, state }));
|
const [, , res] = await apiInterceptors(updateFlowById(id, { name, label, description, editable, uid: id, flow_data: reactFlowObject, state }));
|
||||||
setIsModalVisible(false);
|
setIsModalVisible(false);
|
||||||
@@ -201,11 +231,22 @@ const Canvas: React.FC<Props> = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MuiLoading visible={loading} />
|
<MuiLoading visible={loading} />
|
||||||
<div className="my-2 mx-4 flex flex-row justify-end items-center">
|
<Space className="my-2 mx-4 flex flex-row justify-end">
|
||||||
<div className="w-8 h-8 rounded-md bg-stone-300 dark:bg-zinc-700 dark:text-zinc-200 flext justify-center items-center hover:text-blue-500 dark:hover:text-zinc-100">
|
{[
|
||||||
<SaveOutlined className="block text-xl" onClick={clickSave} />
|
{ title: 'import', icon: <ImportOutlined className="block text-xl" onClick={onImport} /> },
|
||||||
</div>
|
{ title: 'export', icon: <ExportOutlined className="block text-xl" onClick={onExport} /> },
|
||||||
</div>
|
{ title: 'save', icon: <SaveOutlined className="block text-xl" onClick={onSave} /> },
|
||||||
|
].map(({ title, icon }) => (
|
||||||
|
<Tooltip
|
||||||
|
key={title}
|
||||||
|
title={title}
|
||||||
|
className="w-8 h-8 rounded-md bg-stone-300 dark:bg-zinc-700 dark:text-zinc-200 hover:text-blue-500 dark:hover:text-zinc-100"
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
|
||||||
<Divider className="mt-0 mb-0" />
|
<Divider className="mt-0 mb-0" />
|
||||||
<div className="h-[calc(100vh-60px)] w-full" ref={reactFlowWrapper}>
|
<div className="h-[calc(100vh-60px)] w-full" ref={reactFlowWrapper}>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { File } from 'buffer';
|
||||||
import { Node } from 'reactflow';
|
import { Node } from 'reactflow';
|
||||||
|
|
||||||
export type FlowState = 'deployed' | 'developing' | 'initializing' | 'testing' | 'disabled' | 'running' | 'load_failed';
|
export type FlowState = 'deployed' | 'developing' | 'initializing' | 'testing' | 'disabled' | 'running' | 'load_failed';
|
||||||
@@ -12,6 +13,21 @@ export type IFlowUpdateParam = {
|
|||||||
state?: FlowState;
|
state?: FlowState;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type IFlowRefreshParams = {
|
||||||
|
id: string;
|
||||||
|
type_name: string;
|
||||||
|
type_cls: string;
|
||||||
|
flow_type: 'resource' | 'operator';
|
||||||
|
refresh: {
|
||||||
|
name: string;
|
||||||
|
depends?: Array<{
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
has_value: boolean;
|
||||||
|
}>;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
export type IFlow = {
|
export type IFlow = {
|
||||||
dag_id: string;
|
dag_id: string;
|
||||||
gmt_created: string;
|
gmt_created: string;
|
||||||
@@ -60,8 +76,8 @@ export type IFlowNodeParameterUI = {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
editor: {
|
editor: {
|
||||||
width: Number;
|
width: number;
|
||||||
height: Number;
|
height: number;
|
||||||
};
|
};
|
||||||
show_input: boolean;
|
show_input: boolean;
|
||||||
};
|
};
|
||||||
@@ -150,3 +166,29 @@ export type IFlowData = {
|
|||||||
edges: Array<IFlowDataEdge>;
|
edges: Array<IFlowDataEdge>;
|
||||||
viewport: IFlowDataViewport;
|
viewport: IFlowDataViewport;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type IFlowExportParams = {
|
||||||
|
export_type?: 'json' | 'dbgpts';
|
||||||
|
format?: 'json' | 'file';
|
||||||
|
file_name?: string;
|
||||||
|
user_name?: string;
|
||||||
|
sys_code?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IFlowImportParams = {
|
||||||
|
file: File;
|
||||||
|
save_flow?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IUploadFileRequestParams = {
|
||||||
|
files: Array<File>;
|
||||||
|
user_name?: string;
|
||||||
|
sys_code?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IUploadFileResponse = {
|
||||||
|
file_name: string;
|
||||||
|
file_id: string;
|
||||||
|
bucket: string;
|
||||||
|
uri?: string;
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user