diff --git a/.gitignore b/.gitignore index 89bd235..0203dc6 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,6 @@ target/ ._* .Spotlight-V100 .Trashes -Icon? ehthumbs.db Thumbs.db .factorypath diff --git a/react-ui/src/assets/img/modal-select-dataset.png b/react-ui/src/assets/img/modal-select-dataset.png new file mode 100644 index 0000000..6a99cc2 Binary files /dev/null and b/react-ui/src/assets/img/modal-select-dataset.png differ diff --git a/react-ui/src/components/KFModal/index.tsx b/react-ui/src/components/KFModal/index.tsx index 3a03e30..ba3233c 100644 --- a/react-ui/src/components/KFModal/index.tsx +++ b/react-ui/src/components/KFModal/index.tsx @@ -1,14 +1,18 @@ -// 自定义 Modal +/* + * @Author: 赵伟 + * @Date: 2024-04-15 10:01:29 + * @Description: 自定义 Modal + */ import ModalTitle from '@/components/ModalTitle'; import { Modal, type ModalProps } from 'antd'; import classNames from 'classnames'; import './index.less'; -type KFModalProps = ModalProps & { +export interface KFModalProps extends ModalProps { image: string; -}; -function KFModal({ title, image, children, className, ...rest }: KFModalProps) { +} +function KFModal({ title, image, children, className = '', ...rest }: KFModalProps) { return ( .anticon{ +.ant-modal-confirm-confirm .ant-modal-confirm-body > .anticon { display: none; } .ant-modal-confirm .ant-modal-confirm-btns { margin-top: 30px; text-align: center; } -.ant-modal-confirm-btns .ant-btn-default{ - width:91px; -height:42px; -background:rgba(22, 100, 255, 0.06); -border-radius:10px; -color:#1d1d20; -font-size:16px; -margin-right: 10px; +.ant-modal-confirm-btns .ant-btn-default { + width: 91px; + height: 42px; + margin-right: 10px; + color: #1d1d20; + font-size: 16px; + background: rgba(22, 100, 255, 0.06); + border-radius: 10px; } -.ant-modal-confirm-btns .ant-btn-default:hover{ - background:rgba(22, 100, 255, 0.06); +.ant-modal-confirm-btns .ant-btn-default:hover { + background: rgba(22, 100, 255, 0.06); border-color: transparent; } -.ant-modal-confirm-btns .ant-btn-primary{ - width:91px; - height:42px; - background:#1664ff; - border-radius:10px; +.ant-modal-confirm-btns .ant-btn-primary { + width: 91px; + height: 42px; font-size: 16px; + background: #1664ff; + border-radius: 10px; } -.ant-modal .ant-modal-close-x{ - border: 2px solid #272536; - border-radius: 50%; +.ant-modal .ant-modal-close-x { width: 26px; height: 26px; color: #272536; + border: 2px solid #272536; + border-radius: 50%; } -.ant-modal-content{ - margin-left: -130px; +.ant-modal-content { margin-top: 50px; - + margin-left: -130px; } -.ant-modal .ant-modal-content{ +.ant-modal .ant-modal-content { padding: 0; } -.ant-modal-confirm-body-wrapper{ -height:303px; -border-radius:21px; -background-image: url(/assets/images/modal-back.png); -background-repeat:no-repeat; -background-size:100%; -background-position: top center; +.ant-modal-confirm-body-wrapper { + height: 303px; + background-image: url(/assets/images/modal-back.png); + background-repeat: no-repeat; + background-position: top center; + background-size: 100%; + border-radius: 21px; } .ant-modal .ant-modal-close:hover { background-color: transparent; } -.ant-modal .ant-modal-footer >.ant-btn+.ant-btn{ +.ant-modal .ant-modal-footer > .ant-btn + .ant-btn { margin-left: 20px; } .ant-pagination .ant-pagination-item-active a { color: #fff; background: #1664ff; border-color: #1664ff; - border-radius:6px; + border-radius: 6px; } .ant-pagination .ant-pagination-item-active:hover { color: #fff; background: rgba(22, 100, 255, 0.8); border-color: rgba(22, 100, 255, 0.8); - border-radius:6px; + border-radius: 6px; } .ant-pagination .ant-pagination-item { border: 1px solid #e6e6e6; - border-radius:6px; + border-radius: 6px; } // ::-webkit-scrollbar-button { // background: #97a1bd; @@ -195,3 +193,7 @@ ol { } } } + +.local-svg { + vertical-align: -1px; +} diff --git a/react-ui/src/hooks/index.ts b/react-ui/src/hooks/index.ts index 8e0f60c..7ebf7b1 100644 --- a/react-ui/src/hooks/index.ts +++ b/react-ui/src/hooks/index.ts @@ -24,7 +24,7 @@ export function useStateRef(initialValue: T) { * @param initialValue - The initial visibility state of the modal. * @return An array containing the visibility state and functions to open and close the modal. */ -export function useAntdModal(initialValue: boolean) { +export function useVisible(initialValue: boolean) { const [visible, setVisible] = useState(initialValue); const open = useCallback(() => { diff --git a/react-ui/src/icons/dataset-select-button.svg b/react-ui/src/icons/dataset-select-button.svg new file mode 100644 index 0000000..2a376d7 --- /dev/null +++ b/react-ui/src/icons/dataset-select-button.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-ui/src/icons/mirror-select-button.svg b/react-ui/src/icons/mirror-select-button.svg new file mode 100644 index 0000000..59e5a21 --- /dev/null +++ b/react-ui/src/icons/mirror-select-button.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-ui/src/icons/modal-close.svg b/react-ui/src/icons/modal-close.svg new file mode 100644 index 0000000..1345011 --- /dev/null +++ b/react-ui/src/icons/modal-close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-ui/src/icons/model-select-button.svg b/react-ui/src/icons/model-select-button.svg new file mode 100644 index 0000000..525e061 --- /dev/null +++ b/react-ui/src/icons/model-select-button.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-ui/src/icons/parameter.svg b/react-ui/src/icons/parameter.svg new file mode 100644 index 0000000..5880352 --- /dev/null +++ b/react-ui/src/icons/parameter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-ui/src/icons/view-param.svg b/react-ui/src/icons/view-param.svg new file mode 100644 index 0000000..3eb2efc --- /dev/null +++ b/react-ui/src/icons/view-param.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-ui/src/pages/Experiment/experimentText/index.jsx b/react-ui/src/pages/Experiment/experimentText/index.jsx index 15fd0f5..e9f181c 100644 --- a/react-ui/src/pages/Experiment/experimentText/index.jsx +++ b/react-ui/src/pages/Experiment/experimentText/index.jsx @@ -1,5 +1,5 @@ import { ReactComponent as ViewParam } from '@/assets/svg/view-param.svg'; -import { useAntdModal } from '@/hooks'; +import { useVisible } from '@/hooks'; import { getExperimentIns } from '@/services/experiment/index.js'; import { getWorkflowById } from '@/services/pipeline/index.js'; import { elapsedTime } from '@/utils/date'; @@ -22,7 +22,7 @@ function ExperimentText() { const navgite = useNavigate(); const locationParams = useParams(); //新版本获取路由参数接口 let graph = null; - const [paramsModalOpen, openParamsModal, closeParamsModal] = useAntdModal(false); + const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); const timers = (time) => { let timer = new Date(time); diff --git a/react-ui/src/pages/Experiment/index.jsx b/react-ui/src/pages/Experiment/index.jsx index 1c8686a..b79821f 100644 --- a/react-ui/src/pages/Experiment/index.jsx +++ b/react-ui/src/pages/Experiment/index.jsx @@ -143,16 +143,16 @@ function Experiment() { const [res] = await to(getTensorBoardStatusReq(params)); if (res && res.data) { setExperimentInList((prevList) => { - const newList = [...prevList]; - const index = prevList.findIndex((item) => item.id === experimentIn.id); - const preObj = prevList[index]; - const newObj = { - ...preObj, - tensorBoardStatus: res.data.status, - tensorboardUrl: res.data.url, - }; - newList.splice(index, 1, newObj); - return newList; + return prevList.map((item) => { + if (item.id === experimentIn.id) { + return { + ...item, + tensorBoardStatus: res.data.status, + tensorboardUrl: res.data.url, + }; + } + return item; + }); }); const timerId = setTimeout(() => { getTensorBoardStatus(experimentIn); diff --git a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.less b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.less new file mode 100644 index 0000000..6126cf3 --- /dev/null +++ b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.less @@ -0,0 +1,81 @@ +@import '@/styles/theme.less'; + +.model-tabs { + :global { + .ant-tabs-tab { + padding-top: 0 !important; + } + .ant-tabs-nav::before, + div > .ant-tabs-nav::before { + border: none !important; + } + } +} + +.model-selector { + display: flex; + align-items: flex-start; + + &__left { + width: 488px; + height: 398px; + margin-right: 15px; + padding: 15px; + background-color: @background-color-primay; + border: 1px solid @border-color; + border-radius: 8px; + + &__search { + margin-bottom: 14px; + background-color: transparent; + border-width: 0; + border-bottom: 1px solid @border-color-second; + border-radius: 0; + + // &:hover { + // background-color: transparent; + // } + // &:active { + // background-color: transparent; + // } + // &:focus { + // background-color: transparent; + // } + } + } + + &__right { + width: calc(100% - 488px - 15px); + height: 398px; + padding: 15px; + background-color: @background-color-primay; + border: 1px solid @border-color; + border-radius: 8px; + + &__title { + margin-bottom: 15px; + padding: 3px 0 6px; + color: @text-color; + font-size: @font-size; + border-bottom: 1px solid @border-color-second; + } + &__files { + height: calc(100% - 75px); + overflow-y: auto; + + &__file { + height: 24px; + margin-bottom: 10px; + padding-left: 10px; + overflow: hidden; + color: #575757; + font-size: 13px; + line-height: 24px; + white-space: nowrap; + text-overflow: ellipsis; + background: @background-color-gray; + border-radius: 4px; + } + } + } +} diff --git a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx new file mode 100644 index 0000000..1532af3 --- /dev/null +++ b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx @@ -0,0 +1,396 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-11 16:31:18 + * @Description: 选择数据集和模型 + */ + +import datasetImg from '@/assets/img/modal-select-dataset.png'; +import modelImg from '@/assets/img/modal-select-model.png'; +import KFModal from '@/components/KFModal'; +import { + getDatasetList, + getDatasetVersionIdList, + getDatasetVersionsById, + getModelList, + getModelVersionIdList, + getModelVersionsById, +} from '@/services/dataset/index.js'; +import { to } from '@/utils/promise'; +import type { GetRef, ModalProps, TabsProps, TreeDataNode, TreeProps } from 'antd'; +import { Input, Tabs, Tree } from 'antd'; +import React, { useEffect, useRef, useState } from 'react'; +import styles from './index.less'; + +export enum ResourceSelectorType { + Model = 'Model', // 模型 + Dataset = 'Dataset', // 数据集 +} + +type ResourceSelectorTypeKeys = keyof typeof ResourceSelectorType; +type ResourceSelectorTypeValues = (typeof ResourceSelectorType)[ResourceSelectorTypeKeys]; + +type GetModelFilesReqParam = { + model_id: number; + version: string; +}; + +type GetDatasetFilesReqParam = { + dataset_id: number; + version: string; +}; + +type GetFilesReqParam = GetModelFilesReqParam | GetDatasetFilesReqParam; + +export type SelectorTypeInfo = { + getList: (params: { page: number; size: number; available_range: string }) => Promise; + getVersions: (params: number) => Promise; + getFiles: (params: GetFilesReqParam) => Promise; + modalIcon: string; + buttonIcon: string; + name: string; + fileReqParamKey: 'models_id' | 'dataset_id'; + tabItems: TabsProps['items']; +}; + +export const selectorTypeData: Record = { + Model: { + getList: getModelList, + getVersions: getModelVersionsById, + getFiles: getModelVersionIdList, + name: '模型', + modalIcon: modelImg, + buttonIcon: 'local:model-select-button', + fileReqParamKey: 'models_id', + tabItems: [ + { + key: '0', + label: '我的模型', + }, + { + key: '1', + label: '公开模型', + }, + ], + }, + Dataset: { + getList: getDatasetList, + getVersions: getDatasetVersionsById, + getFiles: getDatasetVersionIdList, + name: '数据集', + modalIcon: datasetImg, + buttonIcon: 'local:dataset-select-button', + fileReqParamKey: 'dataset_id', + tabItems: [ + { + key: '0', + label: '我的数据集', + }, + { + key: '1', + label: '公开数据集', + }, + ], + }, +}; + +type ResourceSelectorResponse = { + id: number; + name: string; + path: string; + version: string; + activeTab: string; +}; + +interface ResourceSelectorModalProps extends Omit { + type: ResourceSelectorType; // 模型 | 数据集 + defaultExpandedKeys: React.Key[]; + defaultCheckedKeys: React.Key[]; + defaultActiveTab: string; + onOk?: (params: ResourceSelectorResponse | null) => void; +} + +type ResourceGroup = { + id: number; + name: string; +}; + +type ResourceFile = { + id: number; + file_name: string; +}; + +// list 转成 treeData +const convertToTreeData = (list: ResourceGroup[]): TreeDataNode[] => { + return list.map((v) => ({ + title: v.name, + key: v.id, + isLeaf: false, + checkable: false, + })); +}; + +const updateChildren = (parentId: number, children: TreeDataNode[]) => { + return (node: TreeDataNode) => { + if (node.key === parentId) { + return { + ...node, + children, + }; + } + return node; + }; +}; + +type TreeRef = GetRef>; + +function ResourceSelectorModal({ + type, + defaultExpandedKeys = [], + defaultCheckedKeys = [], + defaultActiveTab = '0', + onOk, + ...rest +}: ResourceSelectorModalProps) { + const [activeTab, setActiveTab] = useState(defaultActiveTab); + const [expandedKeys, setExpandedKeys] = useState([]); + const [checkedKeys, setCheckedKeys] = useState([]); + const [loadedKeys, setLoadedKeys] = useState([]); + const [originTreeData, setOriginTreeData] = useState([]); + const [treeData, setTreeData] = useState([]); + const [files, setFiles] = useState([]); + const [versionPath, setVersionPath] = useState(''); + const [searchText, setSearchText] = useState(''); + const [fisrtLoadList, setFisrtLoadList] = useState(false); + const [fisrtLoadVersions, setFisrtLoadVersions] = useState(false); + const treeRef = useRef(null); + + useEffect(() => { + setExpandedKeys([]); + setCheckedKeys([]); + setLoadedKeys([]); + setFiles([]); + setVersionPath(''); + setSearchText(''); + getTreeData(); + }, [activeTab, type]); + + useEffect(() => { + const treeData = originTreeData.filter((v) => + (v.title as string).toLowerCase()?.includes(searchText.toLowerCase()), + ); + setTreeData(treeData); + }, [originTreeData, searchText]); + + // 获取数据集或模型列表 + const getTreeData = async () => { + const params = { + page: 0, + size: 200, + available_range: activeTab, + }; + const getListReq = selectorTypeData[type].getList; + const [res] = await to(getListReq(params)); + if (res) { + const list = res.data?.content || []; + const treeData = convertToTreeData(list); + setOriginTreeData(treeData); + + // 恢复上一次的 Expand 操作 + restoreLastExpand(); + } else { + setOriginTreeData([]); + } + }; + + // 获取数据集或模型版本列表 + const getVersions = async (parentId: number) => { + const getVersionsReq = selectorTypeData[type].getVersions; + const [res, error] = await to(getVersionsReq(parentId)); + if (res) { + const list = res.data || []; + const children = list.map((v: string) => ({ + title: v, + key: `${parentId}-${v}`, + isLeaf: true, + checkable: true, + })); + // 更新 treeData children + setOriginTreeData((prev) => prev.map(updateChildren(parentId, children))); + // 缓存 loadedKeys + const index = loadedKeys.find((v) => v === parentId); + if (!index) { + setLoadedKeys((prev) => prev.concat(parentId)); + } + // 恢复上一次的 Check 操作 + restoreLastCheck(parentId); + } else { + setExpandedKeys([]); + return Promise.reject(error); + } + }; + + // 获取版本下的文件 + const getFiles = async (id: number, version: string) => { + const getFilesReq = selectorTypeData[type].getFiles; + const paramsKey = selectorTypeData[type].fileReqParamKey; + const params = { version: version, [paramsKey]: id } as GetFilesReqParam; + const [res] = await to(getFilesReq(params)); + if (res) { + setVersionPath(res.data?.path || ''); + setFiles(res.data?.content || []); + } else { + setVersionPath(''); + setFiles([]); + } + }; + + // 动态加载 tree children + const onLoadData = ({ key, children }: TreeDataNode) => { + if (children) { + return Promise.resolve(); + } else { + return getVersions(key as number); + } + }; + + // 扩展 + const onExpand: TreeProps['onExpand'] = (expandedKeysValue) => { + const lastKeys = (expandedKeysValue as React.Key[]).slice(-1); + setExpandedKeys(lastKeys); + }; + + // 选中 + const onCheck: TreeProps['onCheck'] = (checkedKeysValue) => { + const lastKeys = (checkedKeysValue as React.Key[]).slice(-1); + setCheckedKeys(lastKeys); + if (lastKeys.length) { + const last = lastKeys[0] as string; + const index = last.indexOf('-'); + const id = Number(last.slice(0, index)); + const version = last.slice(index + 1); + getFiles(id, version); + } else { + setFiles([]); + } + }; + + // 恢复上一次的 Expand 操作 + // 判断是否有 defaultExpandedKeys,如果有,设置 expandedKeys + // fisrtLoadList 标志位 + const restoreLastExpand = () => { + if (!fisrtLoadList && defaultExpandedKeys.length > 0) { + setTimeout(() => { + setExpandedKeys(defaultExpandedKeys); + setFisrtLoadList(true); + setTimeout(() => { + treeRef.current?.scrollTo({ key: defaultExpandedKeys[0], align: 'bottom' }); + }, 100); + }, 0); + } + }; + + // 恢复上一次的 Check 操作 + // 判断是否有 defaultCheckedKeys,如果有,设置 checkedKeys,并且调用获取文件列表接口 + // fisrtLoadVersions 标志位 + const restoreLastCheck = (parentId: number) => { + if (!fisrtLoadVersions && defaultCheckedKeys.length > 0) { + const last = defaultCheckedKeys[0] as string; + const index = last.indexOf('-'); + const id = Number(last.slice(0, index)); + const version = last.slice(index + 1); + // 判断正在打开的 id 和 defaultCheckedKeys 的 id 是否一致 + if (id === parentId) { + setTimeout(() => { + setCheckedKeys(defaultCheckedKeys); + getFiles(id, version); + setFisrtLoadVersions(true); + setTimeout(() => { + treeRef?.current?.scrollTo({ + key: defaultCheckedKeys[0], + align: 'bottom', + }); + }, 100); + }, 0); + } + } + }; + + // 提交 + const handleOk = () => { + if (checkedKeys.length > 0) { + const last = checkedKeys[0] as string; + const index = last.indexOf('-'); + const id = Number(last.slice(0, index)); + const version = last.slice(index + 1); + const name = (treeData.find((v) => Number(v.key) === id)?.title ?? '') as string; + const res = { + id, + name, + path: versionPath, + version, + activeTab, + }; + onOk?.(res); + } else { + onOk?.(null); + } + }; + + const title = `选择${selectorTypeData[type].name}`; + const palceholder = `请输入${selectorTypeData[type].name}名称`; + const fileTitle = `已选${selectorTypeData[type].name}文件(${files.length})`; + const tabItems = selectorTypeData[type].tabItems; + const titleImg = selectorTypeData[type].modalIcon; + + return ( + +
+ +
+
+ setSearchText(e.target.value)} + /> + +
+
+
{fileTitle}
+
+ {files.map((v) => ( +
+ {v.file_name} +
+ ))} +
+
+
+
+
+ ); +} + +export default ResourceSelectorModal; diff --git a/react-ui/src/pages/Pipeline/editPipeline/editPipeline.less b/react-ui/src/pages/Pipeline/editPipeline/editPipeline.less index f4074a5..af92874 100644 --- a/react-ui/src/pages/Pipeline/editPipeline/editPipeline.less +++ b/react-ui/src/pages/Pipeline/editPipeline/editPipeline.less @@ -51,3 +51,19 @@ color: #ffffff; background-color: rgba(24, 144, 255, 0.3); } + +.ref-row { + display: flex; + align-items: center; + + .select-button { + display: flex; + flex: none; + align-items: center; + justify-content: flex-start; + width: 100px; + margin-left: 10px; + padding-right: 0; + padding-left: 0; + } +} diff --git a/react-ui/src/pages/Pipeline/editPipeline/index.jsx b/react-ui/src/pages/Pipeline/editPipeline/index.jsx index 094735c..02ea0d0 100644 --- a/react-ui/src/pages/Pipeline/editPipeline/index.jsx +++ b/react-ui/src/pages/Pipeline/editPipeline/index.jsx @@ -1,5 +1,5 @@ import { ReactComponent as ParameterIcon } from '@/assets/svg/parameter.svg'; -import { useAntdModal } from '@/hooks'; +import { useVisible } from '@/hooks'; import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js'; import { to } from '@/utils/promise'; import { SaveOutlined } from '@ant-design/icons'; @@ -60,7 +60,7 @@ const EditPipeline = () => { }); const graphRef = useRef(); const paramsDrawerRef = useRef(); - const [paramsDrawerOpen, openParamsDrawer, closeParamsDrawer] = useAntdModal(false); + const [paramsDrawerOpen, openParamsDrawer, closeParamsDrawer] = useVisible(false); const [globalParam, setGlobalParam] = useState([]); const onDragEnd = (val) => { @@ -97,6 +97,7 @@ const EditPipeline = () => { const [res, error] = await to(paramsDrawerRef.current.getFieldsValue()); if (error) { message.error('全局参数配置有误'); + openParamsDrawer(); return; } const data = graph.save(); diff --git a/react-ui/src/pages/Pipeline/editPipeline/props.jsx b/react-ui/src/pages/Pipeline/editPipeline/props.jsx index 5b22cdb..91e9fcc 100644 --- a/react-ui/src/pages/Pipeline/editPipeline/props.jsx +++ b/react-ui/src/pages/Pipeline/editPipeline/props.jsx @@ -1,11 +1,20 @@ -import { Drawer, Form, Input } from 'antd'; +import { openAntdModal } from '@/utils/modal'; +import { Icon } from '@umijs/max'; +import { Button, Drawer, Form, Input } from 'antd'; import { forwardRef, useImperativeHandle, useState } from 'react'; +import ResourceSelectorModal, { ResourceSelectorType } from '../components/ResourceSelectorModal'; import Styles from './editPipeline.less'; const { TextArea } = Input; const Props = forwardRef(({ onParentChange }, ref) => { const [form] = Form.useForm(); const [stagingItem, setStagingItem] = useState({}); const [open, setOpen] = useState(false); + // const [modelSelectorOpen, openModelSelector, closeModelSelector] = useVisible(false); + // const [selectorType, setSelectorType] = useState(SelectorType.Model); + // const [formItemName, setFormItemName] = useState(''); + const [selectedModel, setSelectedModel] = useState(undefined); + const [selectedDataset, setSelectedDataset] = useState(undefined); + const afterOpenChange = () => { if (!open) { console.log(111, open); @@ -76,6 +85,67 @@ const Props = forwardRef(({ onParentChange }, ref) => { } }, })); + + const selectResource = (name, item) => { + // setFormItemName(name); + // setSelectorType(item.item_type === 'dataset' ? SelectorType.Dataset : SelectorType.Model); + // openModelSelector(); + const type = + item.item_type === 'dataset' ? ResourceSelectorType.Dataset : ResourceSelectorType.Model; + const resource = type === ResourceSelectorType.Dataset ? selectedDataset : selectedModel; + const { destroy } = openAntdModal( + ResourceSelectorModal, + { + type, + defaultExpandedKeys: resource ? [resource.id] : [], + defaultCheckedKeys: resource ? [`${resource.id}-${resource.version}`] : [], + defaultActiveTab: resource?.activeTab, + onOk: (res) => { + if (res) { + const value = JSON.stringify(res); + form.setFieldValue(name, value); + if (type === ResourceSelectorType.Dataset) { + setSelectedDataset(res); + } else { + setSelectedModel(res); + } + } else { + if (type === ResourceSelectorType.Dataset) { + setSelectedDataset(null); + } else { + setSelectedModel(null); + } + form.setFieldValue(name, ''); + } + destroy(); + }, + }, + true, + ); + }; + + const handleModelSelect = (obj) => { + if (obj) { + const value = JSON.stringify(obj); + setSelectedModel(obj); + form.setFieldValue(formItemName, value); + } else { + form.setFieldValue(formItemName, ''); + } + closeModelSelector(); + }; + + const getSelectBtnIcon = (item) => { + const type = item.item_type; + if (type === 'dataset') { + return ; + } else if (type === 'model') { + return ; + } else { + return ; + } + }; + return ( <> { form={form} layout="vertical" labelCol={{ - span: 16, + span: 24, }} wrapperCol={{ - span: 16, + span: 24, }} style={{ maxWidth: 600, @@ -188,7 +258,7 @@ const Props = forwardRef(({ onParentChange }, ref) => { Object.keys(stagingItem.control_strategy) && Object.keys(stagingItem.control_strategy).length > 0 ? Object.keys(stagingItem.control_strategy).map((item) => ( - + )) @@ -206,11 +276,28 @@ const Props = forwardRef(({ onParentChange }, ref) => { Object.keys(stagingItem.in_parameters).length > 0 ? Object.keys(stagingItem.in_parameters).map((item) => ( - +
+ + + + + + +
)) : ''} @@ -227,6 +314,7 @@ const Props = forwardRef(({ onParentChange }, ref) => { Object.keys(stagingItem.out_parameters).length > 0 ? Object.keys(stagingItem.out_parameters).map((item) => ( { )) : ''} + {/* {modelSelectorOpen && ( + + )} */}
); diff --git a/react-ui/src/requestConfig.ts b/react-ui/src/requestConfig.ts index 1036bc8..a742d3c 100644 --- a/react-ui/src/requestConfig.ts +++ b/react-ui/src/requestConfig.ts @@ -1,4 +1,9 @@ -import type { RequestConfig } from '@umijs/max'; +/* + * @Author: 赵伟 + * @Date: 2024-03-25 13:52:54 + * @Description: + */ +import type { RequestConfig } from '@umijs/max'; import { message } from 'antd'; import { clearSessionToken, getAccessToken, getRefreshToken, getTokenExpireTime } from './access'; diff --git a/react-ui/src/utils/modal.tsx b/react-ui/src/utils/modal.tsx new file mode 100644 index 0000000..b994d3e --- /dev/null +++ b/react-ui/src/utils/modal.tsx @@ -0,0 +1,88 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-13 10:08:35 + * @Description: + */ +import { type ModalProps } from 'antd'; +import React, { useState } from 'react'; +import { createRoot } from 'react-dom/client'; + +/** + * Function to open an Ant Design modal. + * + * @param modal - The function that renders the modal content. + * @param modalProps - The modal properties. + * @return An object with a destroy method to close the modal. + */ +export const openAntdModal = ( + modal: (props: T) => React.ReactNode, + modalProps: T, +) => { + const CustomModel = modal; + const element = document.createElement('div'); + element.id = 'modal-container'; + document.body.appendChild(element); + const root = createRoot(element); + const { afterClose, onCancel } = modalProps; + + function destroy() { + root.unmount(); + document.body.removeChild(element); + } + + function handleAfterClose() { + afterClose?.(); + setTimeout(() => { + destroy(); + }, 0); + } + + function handleCancel(e: React.MouseEvent) { + if (onCancel) { + onCancel(e); + } else { + close(); + } + } + + function render(props: T) { + root.render(); + } + + function close() { + render({ ...modalProps, open: false, afterClose: handleAfterClose }); + } + + render({ ...modalProps, open: true }); + + return { + destroy: close, + }; +}; + +/** + * Generates a custom hook for managing an Ant Design modal. + * + * @param modal - The function that renders the modal content. + * @param key - The key for the modal. + * @return The modal component, open function, and close function. + */ +export const useAntdModal = ( + modal: (props: T) => React.ReactNode, + key: React.Key, +) => { + const [visible, setVisible] = useState(false); + const [props, setProps] = useState({} as T); + const CustomModel = modal; + + const open = (props: T) => { + setProps(props); + setVisible(true); + }; + + const close = () => { + setVisible(false); + }; + + return [, open, close] as const; +};