diff --git a/.gitignore b/.gitignore index 89bd235d..0203dc65 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,6 @@ target/ ._* .Spotlight-V100 .Trashes -Icon? ehthumbs.db Thumbs.db .factorypath diff --git a/react-ui/config/defaultSettings.ts b/react-ui/config/defaultSettings.ts index b4430310..35d75e3f 100644 --- a/react-ui/config/defaultSettings.ts +++ b/react-ui/config/defaultSettings.ts @@ -19,7 +19,7 @@ const Settings: ProLayoutProps & { title: '复杂智能软件', pwa: true, logo: '/assets/images/left-top-logo.png', - iconfontUrl: '', + iconfontUrl: '//at.alicdn.com/t/c/font_4509211_dfghcwme8ki.js', token: { // 参见ts声明,demo 见文档,通过token 修改样式 //https://procomponents.ant.design/components/layout#%E9%80%9A%E8%BF%87-token-%E4%BF%AE%E6%94%B9%E6%A0%B7%E5%BC%8F diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts index 9c30003d..959bf0aa 100644 --- a/react-ui/config/routes.ts +++ b/react-ui/config/routes.ts @@ -135,6 +135,17 @@ export default [ path: '/dataset/datasetIntro/:id', component: './Dataset/datasetIntro', }, + { + name: '镜像', + path: 'mirror', + routes: [ + { + name: '镜像列表', + path: '', + component: './Mirror/list', + }, + ], + }, { name: '模型管理', path: '/dataset/modelIndex', diff --git a/react-ui/public/assets/images/delete-icon.png b/react-ui/public/assets/images/delete-icon.png new file mode 100644 index 00000000..310f8bb8 Binary files /dev/null and b/react-ui/public/assets/images/delete-icon.png differ diff --git a/react-ui/public/assets/images/icon/流水线-1.png b/react-ui/public/assets/images/icon/流水线-1.png new file mode 100644 index 00000000..7fbb9266 Binary files /dev/null and b/react-ui/public/assets/images/icon/流水线-1.png differ diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index ee86b5cd..5c255b73 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -1,7 +1,9 @@ import RightContent from '@/components/RightContent'; +import themes from '@/styles/theme.less'; import type { Settings as LayoutSettings } from '@ant-design/pro-components'; import type { RunTimeLayoutConfig } from '@umijs/max'; import { history } from '@umijs/max'; +import { RuntimeAntdConfig } from 'umi'; import defaultSettings from '../config/defaultSettings'; import '../public/fonts/font.css'; import { getAccessToken } from './access'; @@ -182,3 +184,24 @@ export function render(oldRender: () => void) { oldRender(); }); } + +// 主题修改 +export const antd: RuntimeAntdConfig = (memo) => { + memo.theme ??= {}; + memo.theme.token = { + colorPrimary: themes['primaryColor'], + }; + memo.theme.components ??= {}; + memo.theme.components.Tabs = {}; + // memo.theme.cssVar = true; + // memo.theme.hashed = false; + + // memo.appConfig = { + // message: { + // // 配置 message 最大显示数,超过限制时,最早的消息会被自动关闭 + // maxCount: 3, + // }, + // }; + + return memo; +}; diff --git a/react-ui/src/assets/img/mirror-tabs-bg.png b/react-ui/src/assets/img/mirror-tabs-bg.png new file mode 100644 index 00000000..9e01b8f3 Binary files /dev/null and b/react-ui/src/assets/img/mirror-tabs-bg.png differ 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 00000000..6a99cc24 Binary files /dev/null and b/react-ui/src/assets/img/modal-select-dataset.png differ diff --git a/react-ui/src/assets/svg/save--return.svg b/react-ui/src/assets/svg/save--return.svg new file mode 100644 index 00000000..73fd196d --- /dev/null +++ b/react-ui/src/assets/svg/save--return.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-ui/src/components/KFModal/index.tsx b/react-ui/src/components/KFModal/index.tsx index 3a03e303..3491ca6b 100644 --- a/react-ui/src/components/KFModal/index.tsx +++ b/react-ui/src/components/KFModal/index.tsx @@ -1,22 +1,35 @@ -// 自定义 Modal +/* + * @Author: 赵伟 + * @Date: 2024-04-15 10:01:29 + * @Description: 自定义 Modal + */ import ModalTitle from '@/components/ModalTitle'; -import { Modal, type ModalProps } from 'antd'; +import { ConfigProvider, Modal, theme, type ModalProps } from 'antd'; import classNames from 'classnames'; +import { useAntdConfig } from 'umi'; import './index.less'; -type KFModalProps = ModalProps & { +const { useToken } = theme; + +export interface KFModalProps extends ModalProps { image: string; -}; -function KFModal({ title, image, children, className, ...rest }: KFModalProps) { +} +function KFModal({ title, image, children, className = '', ...rest }: KFModalProps) { + const { token } = useToken(); + console.log('token', token); + const antdConfig = useAntdConfig(); + console.log('antdConfig', antdConfig); return ( - } - > - {children} - + + } + > + {children} + + ); } diff --git a/react-ui/src/components/KFTabs/index.less b/react-ui/src/components/KFTabs/index.less new file mode 100644 index 00000000..e69de29b diff --git a/react-ui/src/components/KFTabs/index.tsx b/react-ui/src/components/KFTabs/index.tsx new file mode 100644 index 00000000..a9a9f2f1 --- /dev/null +++ b/react-ui/src/components/KFTabs/index.tsx @@ -0,0 +1,32 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-16 14:55:40 + * @Description: + */ +// import { useEffect, useState } from 'react'; +// import styles from './index.less'; +// import { Tabs } from "antd" + +// function KFTabs() { +// const [iframeUrl, setIframeUrl] = useState(''); +// useEffect(() => { + +// }, []); + +// return ( +//
+//
+// +// +// +// +// +// +// +// +//
+//
+// ); +// } + +// export default KFTabs; diff --git a/react-ui/src/global.less b/react-ui/src/global.less index 29b900b5..a50f3ec4 100644 --- a/react-ui/src/global.less +++ b/react-ui/src/global.less @@ -31,10 +31,10 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -a{ - color:#1664ff; +a { + color: #1664ff; } -.ant-btn-link{ +.ant-btn-link { color: #1664ff; } .ant-pro-layout .ant-pro-layout-content { @@ -52,9 +52,8 @@ a{ .ant-menu-light .ant-menu-item-selected { background: rgba(197, 232, 255, 0.8) !important; } -.ant-pro-layout .ant-pro-sider .ant-layout-sider-children{ - background:#f2f5f7; - +.ant-pro-layout .ant-pro-sider .ant-layout-sider-children { + background: #f2f5f7; } .ant-pro-base-menu-inline { // height: 87vh; @@ -64,12 +63,12 @@ a{ .ant-pro-layout .ant-pro-layout-content { background-color: transparent; } -.ant-table-wrapper .ant-table-pagination.ant-pagination{ - background-color: #fff; +.ant-table-wrapper .ant-table-pagination.ant-pagination { margin: 0; padding: 21px 16px; + background-color: #fff; } -.ant-table-wrapper .ant-table{ +.ant-table-wrapper .ant-table { height: 75vh; } .ant-pro-global-header-logo img { @@ -81,38 +80,96 @@ a{ .ant-pro-layout .ant-pro-layout-container { height: 98vh; } -.ant-modal .ant-modal-close-x{ - border: 2px solid #272536; - border-radius: 50%; +.ant-modal-confirm .ant-modal-confirm-paragraph{ + margin: 54px 0 auto; + text-align: center; +} +.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:110px; +height:40px; +background:rgba(22, 100, 255, 0.06); +border-radius:10px; +color:#1d1d20; +font-size:18px; +margin-right: 10px; +border-color: transparent; +} +.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:110px; + height:40px; + background:#1664ff; + border-radius:10px; + font-size: 18px; +} +.ant-modal .ant-input-affix-wrapper{ + height: 46px; + padding: 1px 11px; +} +.ant-modal .ant-select-single{ + height: 46px; + +} +.ant-modal .ant-select-single .ant-select-selector .ant-select-selection-placeholder{ + line-height: 46px; +} +.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 { + 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; +border-radius: 0; +} +.ant-modal .ant-modal-content { + border-radius: 20px; } .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; @@ -152,3 +209,7 @@ ol { } } } + +.umi-local-svg { + vertical-align: -1px; +} diff --git a/react-ui/src/hooks/index.ts b/react-ui/src/hooks/index.ts index 8e0f60c6..7ebf7b1c 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 00000000..2a376d71 --- /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/magnifying-glass.svg b/react-ui/src/icons/magnifying-glass.svg new file mode 100644 index 00000000..69bf383b --- /dev/null +++ b/react-ui/src/icons/magnifying-glass.svg @@ -0,0 +1,3 @@ + + + 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 00000000..59e5a215 --- /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 00000000..1345011e --- /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 00000000..525e061b --- /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 00000000..58803521 --- /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 00000000..3eb2efce --- /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/Dataset/datasetIntro.jsx b/react-ui/src/pages/Dataset/datasetIntro.jsx index bdc9171b..bd7b1604 100644 --- a/react-ui/src/pages/Dataset/datasetIntro.jsx +++ b/react-ui/src/pages/Dataset/datasetIntro.jsx @@ -59,12 +59,14 @@ const Dataset = () => { const locationParams = useParams(); //新版本获取路由参数接口 const [wordList, setWordList] = useState([]); const [activeTabKey, setActiveTabKey] = useState('1'); + const [uuid, setUuid] = useState(Date.now()); const getDatasetByDetail = () => { getDatasetById(locationParams.id).then((ret) => { console.log(ret); setDatasetDetailObj(ret.data); }); }; + // 获取数据集版本 const getDatasetVersionList = () => { getDatasetVersionsById(locationParams.id).then((ret) => { console.log(ret); @@ -77,6 +79,8 @@ const Dataset = () => { }; }), ); + setVersion(ret.data[0]); + getDatasetVersions({ version: ret.data[0], dataset_id: locationParams.id }); } }); }; @@ -90,6 +94,7 @@ const Dataset = () => { form.setFieldsValue({ name: datasetDetailObj.name }); setDialogTitle('创建新版本'); + setUuid(Date.now()); setIsModalOpen(true); }; const handleCancel = () => { @@ -102,16 +107,23 @@ const Dataset = () => { }; const deleteDataset = () => { Modal.confirm({ - title: '删除', - content: '确定删除数据集版本?', + title: ( +
+ +
删除后,该数据集版本将不可恢复
+
+ ), + content:
是否确认删除?
, okText: '确认', cancelText: '取消', onOk: () => { deleteDatasetVersion({ dataset_id: locationParams.id, version }).then((ret) => { - setVersion(null); getDatasetVersionList(); - getDatasetVersions({ version, dataset_id: locationParams.id }); message.success('删除成功'); }); }, @@ -124,6 +136,7 @@ const Dataset = () => { message.success('创建成功'); }); }; + // 获取版本下的文件列表 const getDatasetVersions = (params) => { getDatasetVersionIdList(params).then((res) => { setWordList(res?.data?.content ?? []); @@ -368,7 +381,7 @@ const Dataset = () => { }, ]} > - + , + ], + }, + ]; + + const getMirrorList = async (name: string) => { + const params = { + page: pagination.current - 1, + size: pagination.pageSize, + name, + image_type: 1, + }; + const [res] = await to(getMirrorListReq(params)); + if (res && res.data) { + const { content = [], totalElements = 0 } = res.data; + console.log(res); + setTableData(content); + setPagination((prev) => ({ + ...prev, + total: totalElements, + })); + } + }; + + const onSearch = (value: string) => { + getMirrorList(value); + }; + + return ( +
+
+ {/* */} +
+
+
+ + +
+
+ + + + + ); +} + +export default MirrorList; diff --git a/react-ui/src/pages/Model/modelIntro.jsx b/react-ui/src/pages/Model/modelIntro.jsx index fe683e8b..36478412 100644 --- a/react-ui/src/pages/Model/modelIntro.jsx +++ b/react-ui/src/pages/Model/modelIntro.jsx @@ -101,8 +101,17 @@ const Dataset = () => { }; const deleteDataset = () => { Modal.confirm({ - title: '删除', - content: '确定删除模型版本?', + title: ( +
+ +
删除后,该模型版本将不可恢复
+
+ ), + content:
是否确认删除?
, okText: '确认', cancelText: '取消', 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 00000000..d77d5519 --- /dev/null +++ b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.less @@ -0,0 +1,83 @@ +@import '@/styles/theme.less'; + +.model-tabs { + margin-left: 8px; + :global { + .ant-tabs-tab { + padding-top: 0; + } + .ant-tabs-nav::before, + div > .ant-tabs-nav::before { + border: none; + } + + .ant-tabs-nav { + margin-bottom: 0; + } + } +} + +.model-selector { + display: flex; + align-items: flex-start; + + :global { + .ant-input-affix-wrapper .ant-input-prefix { + margin-inline-end: 12px; + } + } + + &__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; + padding-left: 0; + background-color: transparent; + border-width: 0; + border-bottom: 1px solid @border-color-second; + border-radius: 0; + } + } + + &__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 00000000..213f9453 --- /dev/null +++ b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx @@ -0,0 +1,410 @@ +/* + * @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 { Icon } from '@umijs/max'; +import type { GetRef, ModalProps, TabsProps, TreeDataNode, TreeProps } from 'antd'; +import { Input, Tabs, Tree } from 'antd'; +import React, { useEffect, useMemo, 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 = { + models_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']; +}; + +enum TabItemKeys { + Private = 'Private', // 我的 + Public = 'Public', // 公开 +} + +export const selectorTypeData: Record = { + Model: { + getList: getModelList, + getVersions: getModelVersionsById, + getFiles: getModelVersionIdList, + name: '模型', + modalIcon: modelImg, + buttonIcon: 'local:model-select-button', + fileReqParamKey: 'models_id', + tabItems: [ + { + key: TabItemKeys.Private, + label: '我的模型', + }, + { + key: TabItemKeys.Public, + label: '公开模型', + }, + ], + }, + Dataset: { + getList: getDatasetList, + getVersions: getDatasetVersionsById, + getFiles: getDatasetVersionIdList, + name: '数据集', + modalIcon: datasetImg, + buttonIcon: 'local:dataset-select-button', + fileReqParamKey: 'dataset_id', + tabItems: [ + { + key: TabItemKeys.Private, + label: '我的数据集', + }, + { + key: TabItemKeys.Public, + label: '公开数据集', + }, + ], + }, +}; + +type ResourceSelectorResponse = { + id: number; // 数据集或者模型 id + name: string; // 数据集或者模型 name + version: string; // 数据集或者模型版本 + path: string; // 数据集或者模型版本路径 + activeTab: TabItemKeys; // 是我的还是公开的 +}; + +interface ResourceSelectorModalProps extends Omit { + type: ResourceSelectorType; // 模型 | 数据集 + defaultExpandedKeys: React.Key[]; + defaultCheckedKeys: React.Key[]; + defaultActiveTab: TabItemKeys; + onOk?: (params: ResourceSelectorResponse | null) => void; +} + +type ResourceGroup = { + id: number; // 数据集或者模型 id + name: string; // 数据集或者模型 id +}; + +type ResourceFile = { + id: number; // 文件 id + file_name: string; // 文件 name +}; + +type TreeRef = GetRef>; + +// list 转成 treeData +const convertToTreeData = (list: ResourceGroup[]): TreeDataNode[] => { + return list.map((v) => ({ + title: v.name, + key: v.id, + isLeaf: false, + checkable: false, + })); +}; + +// 更新树形结构的 children +const updateChildren = (parentId: number, children: TreeDataNode[]) => { + return (node: TreeDataNode) => { + if (node.key === parentId) { + return { + ...node, + children, + }; + } + return node; + }; +}; + +// 得到数据集或者模型 id 和下属版本号 +const getIdAndVersion = (versionKey: string) => { + const index = versionKey.indexOf('-'); + const id = Number(versionKey.slice(0, index)); + const version = versionKey.slice(index + 1); + return { + id, + version, + }; +}; + +function ResourceSelectorModal({ + type, + defaultExpandedKeys = [], + defaultCheckedKeys = [], + defaultActiveTab = TabItemKeys.Private, + 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 [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]); + + const treeData = useMemo( + () => + originTreeData.filter((v) => + (v.title as string).toLowerCase()?.includes(searchText.toLowerCase()), + ), + [originTreeData, searchText], + ); + + // 获取数据集或模型列表 + const getTreeData = async () => { + const available_range = activeTab === TabItemKeys.Private ? '0' : '1'; + const params = { + page: 0, + size: 200, + available_range: available_range, + }; + 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.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 { id, version } = getIdAndVersion(last); + 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 { id, version } = getIdAndVersion(last); + // 判断正在打开的 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 { id, version } = getIdAndVersion(last); + const name = (treeData.find((v) => Number(v.key) === id)?.title ?? '') as string; + const res = { + id, + name, + path: versionPath, + version, + activeTab: activeTab as TabItemKeys, + }; + 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)} + prefix={} + /> + +
+
+
{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 f4074a51..af928743 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 094735cf..ec0f6b06 100644 --- a/react-ui/src/pages/Pipeline/editPipeline/index.jsx +++ b/react-ui/src/pages/Pipeline/editPipeline/index.jsx @@ -1,4 +1,5 @@ import { ReactComponent as ParameterIcon } from '@/assets/svg/parameter.svg'; +import { ReactComponent as SaveAndReturn } from '@/assets/svg/save--return.svg'; import { useAntdModal } from '@/hooks'; import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js'; import { to } from '@/utils/promise'; @@ -60,7 +61,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 +98,7 @@ const EditPipeline = () => { const [res, error] = await to(paramsDrawerRef.current.getFieldsValue()); if (error) { message.error('全局参数配置有误'); + openParamsDrawer(); return; } const data = graph.save(); @@ -606,8 +608,8 @@ const EditPipeline = () => { }, }, // linkCenter: true, - fitView: false, - fitViewPadding: [60, 60, 60, 80], + fitView: true, + fitViewPadding: [320, 320, 220, 320], }); graph.on('dblclick', (e) => { console.log(e.item); @@ -722,7 +724,13 @@ const EditPipeline = () => {