diff --git a/react-ui/src/assets/img/404.png b/react-ui/src/assets/img/404.png new file mode 100644 index 00000000..610b7986 Binary files /dev/null and b/react-ui/src/assets/img/404.png differ diff --git a/react-ui/src/assets/img/modal-code-config.png b/react-ui/src/assets/img/modal-code-config.png new file mode 100644 index 00000000..776d4825 Binary files /dev/null and b/react-ui/src/assets/img/modal-code-config.png differ diff --git a/react-ui/src/assets/img/no-data.png b/react-ui/src/assets/img/no-data.png new file mode 100644 index 00000000..d2239289 Binary files /dev/null and b/react-ui/src/assets/img/no-data.png differ diff --git a/react-ui/src/assets/img/usage-icon.png b/react-ui/src/assets/img/usage-icon.png new file mode 100644 index 00000000..cda2cfaf Binary files /dev/null and b/react-ui/src/assets/img/usage-icon.png differ diff --git a/react-ui/src/components/BasicInfo/index.less b/react-ui/src/components/BasicInfo/index.less new file mode 100644 index 00000000..0a07e4aa --- /dev/null +++ b/react-ui/src/components/BasicInfo/index.less @@ -0,0 +1,35 @@ +.kf-basic-info { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 20px 40px; + align-items: flex-start; + width: 80%; + + &__item { + display: flex; + align-items: flex-start; + width: calc(50% - 20px); + font-size: 16px; + line-height: 1.6; + + &__label { + width: 100px; + color: @text-color-secondary; + text-align: justify; + text-align-last: justify; + } + + &__value { + display: flex; + flex: 1; + color: @text-color; + word-break: break-all; + + &::before { + margin-right: 16px; + content: ':'; + } + } + } +} diff --git a/react-ui/src/components/BasicInfo/index.tsx b/react-ui/src/components/BasicInfo/index.tsx new file mode 100644 index 00000000..d0253dd8 --- /dev/null +++ b/react-ui/src/components/BasicInfo/index.tsx @@ -0,0 +1,27 @@ +import './index.less'; +export type BasicInfoData = { + label: string; + value?: any; + format?: (_value: any) => string; +}; + +type BasicInfoProps = { + datas: BasicInfoData[]; +}; + +function BasicInfo({ datas }: BasicInfoProps) { + return ( +
+ {datas.map((item) => ( +
+
{item.label}
+
+ {item.format ? item.format(item.value) ?? '--' : item.value ?? '--'} +
+
+ ))} +
+ ); +} + +export default BasicInfo; diff --git a/react-ui/src/components/KFEmpty/index.less b/react-ui/src/components/KFEmpty/index.less new file mode 100644 index 00000000..9a9cc689 --- /dev/null +++ b/react-ui/src/components/KFEmpty/index.less @@ -0,0 +1,40 @@ +.kf-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + + &__image { + width: 475px; + height: 292px; + } + + &__title { + margin-top: 15px; + color: @text-color; + font-weight: 500; + font-size: 30px; + letter-spacing: 5px; + text-align: center; + } + + &__content { + margin-top: 15px; + color: @text-color-secondary; + font-size: 15px; + white-space: pre-line; + text-align: center; + } + + &__footer { + display: flex; + align-items: center; + justify-content: center; + margin-top: 20px; + + &__back-btn { + height: 32px; + } + } +} diff --git a/react-ui/src/components/KFEmpty/index.tsx b/react-ui/src/components/KFEmpty/index.tsx new file mode 100644 index 00000000..492de75c --- /dev/null +++ b/react-ui/src/components/KFEmpty/index.tsx @@ -0,0 +1,67 @@ +import { Button } from 'antd'; +import classNames from 'classnames'; +import './index.less'; + +export enum EmptyType { + NoData = 'NoData', + NotFound = 'NotFound', + Developing = 'Developing', +} + +type EmptyProps = { + className?: string; + style?: React.CSSProperties; + type: EmptyType; + title?: string; + content?: string; + hasFooter?: boolean; + footer?: () => React.ReactNode; + backTitle?: string; + onBack?: () => void; +}; + +function getEmptyImage(type: EmptyType) { + switch (type) { + case EmptyType.NoData: + return require('@/assets/img/no-data.png'); + case EmptyType.NotFound: + return require('@/assets/img/404.png'); + case EmptyType.Developing: + return require('@/assets/img/missing-back.png'); + } +} + +function KFEmpty({ + className, + style, + type, + title, + content, + hasFooter = false, + footer, + backTitle = '返回', + onBack, +}: EmptyProps) { + const image = getEmptyImage(type); + + return ( +
+ +
{title}
+
{content}
+ {hasFooter && ( +
+ {footer ? ( + footer() + ) : ( + + )} +
+ )} +
+ ); +} + +export default KFEmpty; diff --git a/react-ui/src/components/KFSpin/index.less b/react-ui/src/components/KFSpin/index.less index 931e7ea0..56ff13a1 100644 --- a/react-ui/src/components/KFSpin/index.less +++ b/react-ui/src/components/KFSpin/index.less @@ -4,7 +4,7 @@ right: 0; bottom: 0; left: 0; - z-index: 1000; + z-index: 1001; display: flex; flex-direction: column; align-items: center; diff --git a/react-ui/src/components/ResourceSelect/index.tsx b/react-ui/src/components/ResourceSelect/index.tsx index 96f5e96e..a7ca6514 100644 --- a/react-ui/src/components/ResourceSelect/index.tsx +++ b/react-ui/src/components/ResourceSelect/index.tsx @@ -11,6 +11,7 @@ import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; import './index.less'; export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; +export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse }; type ResourceSelectProps = { type: ResourceSelectorType; diff --git a/react-ui/src/iconfont/iconfont.js b/react-ui/src/iconfont/iconfont.js index 5326fd3a..c9da6580 100644 --- a/react-ui/src/iconfont/iconfont.js +++ b/react-ui/src/iconfont/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_4511447='',(t=>{var a=(h=(h=document.getElementsByTagName("script"))[h.length-1]).getAttribute("data-injectcss"),h=h.getAttribute("data-disable-injectsvg");if(!h){var l,v,z,i,o,m=function(a,h){h.parentNode.insertBefore(a,h)};if(a&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}l=function(){var a,h=document.createElement("div");h.innerHTML=t._iconfont_svg_string_4511447,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(a=document.body).firstChild?m(h,a.firstChild):a.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),l()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(z=l,i=t.document,o=!1,d(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,p())})}function p(){o||(o=!0,z())}function d(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(d,50)}p()}})(window); \ No newline at end of file +window._iconfont_svg_string_4511447='',(t=>{var a=(h=(h=document.getElementsByTagName("script"))[h.length-1]).getAttribute("data-injectcss"),h=h.getAttribute("data-disable-injectsvg");if(!h){var l,v,z,i,o,m=function(a,h){h.parentNode.insertBefore(a,h)};if(a&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}l=function(){var a,h=document.createElement("div");h.innerHTML=t._iconfont_svg_string_4511447,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(a=document.body).firstChild?m(h,a.firstChild):a.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),l()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(z=l,i=t.document,o=!1,d(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,p())})}function p(){o||(o=!0,z())}function d(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(d,50)}p()}})(window); \ No newline at end of file diff --git a/react-ui/src/overrides.less b/react-ui/src/overrides.less index e20ad0e4..af9591fe 100644 --- a/react-ui/src/overrides.less +++ b/react-ui/src/overrides.less @@ -116,6 +116,10 @@ } } + .ant-input.ant-input-disabled { + height: 46px; + } + // 选择框高度为46px .ant-select-single { height: 46px; diff --git a/react-ui/src/pages/404.tsx b/react-ui/src/pages/404.tsx index 0263687e..23242d56 100644 --- a/react-ui/src/pages/404.tsx +++ b/react-ui/src/pages/404.tsx @@ -1,18 +1,20 @@ -import { history } from '@umijs/max'; -import { Button, Result } from 'antd'; -import React from 'react'; +import KFEmpty, { EmptyType } from '@/components/KFEmpty'; +import { useNavigate } from '@umijs/max'; -const NoFoundPage: React.FC = () => ( - history.push('/')}> - Back Home - - } - /> -); +const NoFoundPage = () => { + const navigate = useNavigate(); + + return ( + navigate('/')} + > + ); +}; export default NoFoundPage; diff --git a/react-ui/src/pages/CodeConfig/List/index.tsx b/react-ui/src/pages/CodeConfig/List/index.tsx index 316f5925..1a307751 100644 --- a/react-ui/src/pages/CodeConfig/List/index.tsx +++ b/react-ui/src/pages/CodeConfig/List/index.tsx @@ -1,9 +1,10 @@ +import KFEmpty, { EmptyType } from '@/components/KFEmpty'; import KFIcon from '@/components/KFIcon'; import { deleteCodeConfigReq, getCodeConfigListReq } from '@/services/codeConfig'; import { openAntdModal } from '@/utils/modal'; import { to } from '@/utils/promise'; import { modalConfirm } from '@/utils/ui'; -import { App, Button, Empty, Input, Pagination, PaginationProps } from 'antd'; +import { App, Button, Input, Pagination, PaginationProps } from 'antd'; import { useEffect, useState } from 'react'; import AddCodeConfigModal, { OperationType } from '../components/AddCodeConfigModal'; import CodeConfigItem from '../components/CodeConfigItem'; @@ -31,7 +32,7 @@ export type ResourceListRef = { }; function CodeConfigList() { - const [dataList, setDataList] = useState([]); + const [dataList, setDataList] = useState(undefined); const [total, setTotal] = useState(0); const [pagination, setPagination] = useState({ current: 1, @@ -56,6 +57,9 @@ function CodeConfigList() { if (res && res.data && res.data.content) { setDataList(res.data.content); setTotal(res.data.totalElements); + } else { + setDataList([]); + setTotal(0); } }; @@ -117,7 +121,7 @@ function CodeConfigList() { return (
- 数据总数:{total}个 + 数据总数:{total} 个
- {dataList?.length !== 0 ? ( + {dataList && dataList.length !== 0 && ( <>
- {dataList?.map((item) => ( + {dataList.map((item) => ( - ) : ( -
- -
+ )} + {dataList && dataList.length === 0 && ( + )}
); diff --git a/react-ui/src/pages/Dataset/components/AddDatasetModal/index.less b/react-ui/src/pages/Dataset/components/AddDatasetModal/index.less index 66022fdd..428395bd 100644 --- a/react-ui/src/pages/Dataset/components/AddDatasetModal/index.less +++ b/react-ui/src/pages/Dataset/components/AddDatasetModal/index.less @@ -1,6 +1,7 @@ .upload-tip { margin-top: 5px; - color: @error-color; + color: @text-color-secondary; + font-size: 14px; } .upload-button { diff --git a/react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx b/react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx index 97b93fdf..bf3dc0d1 100644 --- a/react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx +++ b/react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx @@ -1,9 +1,7 @@ import { getAccessToken } from '@/access'; -import { DictValueEnumObj } from '@/components/DictTag'; import KFIcon from '@/components/KFIcon'; import KFModal from '@/components/KFModal'; -import { addDatesetAndVesion } from '@/services/dataset/index.js'; -import { getDictSelectOption } from '@/services/system/dict'; +import { addDateset } from '@/services/dataset/index.js'; import { to } from '@/utils/promise'; import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui'; import { @@ -19,7 +17,7 @@ import { type UploadProps, } from 'antd'; import { omit } from 'lodash'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { CategoryData } from '../../config'; import styles from './index.less'; @@ -31,15 +29,15 @@ interface AddDatasetModalProps extends Omit { function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalProps) { const [uuid] = useState(Date.now()); - const [clusterOptions, setClusterOptions] = useState([]); + // const [clusterOptions, setClusterOptions] = useState([]); - useEffect(() => { - getClusterOptions(); - }, []); + // useEffect(() => { + // getClusterOptions(); + // }, []); // 上传组件参数 const uploadProps: UploadProps = { - action: '/api/mmp/dataset/upload', + action: '/api/mmp/newdataset/upload', headers: { Authorization: getAccessToken() || '', }, @@ -47,16 +45,16 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr }; // 获取集群版本数据 - const getClusterOptions = async () => { - const [res] = await to(getDictSelectOption('available_cluster')); - if (res) { - setClusterOptions(res); - } - }; + // const getClusterOptions = async () => { + // const [res] = await to(getDictSelectOption('available_cluster')); + // if (res) { + // setClusterOptions(res); + // } + // }; // 上传请求 const createDataset = async (params: any) => { - const [res] = await to(addDatesetAndVesion(params)); + const [res] = await to(addDateset(params)); if (res) { message.success('创建成功'); onOk?.(); @@ -94,7 +92,13 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr }} destroyOnClose > -
+ - + @@ -135,14 +139,14 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr allowClear placeholder="请选择研究方向/应用领域" options={tagList} - fieldNames={{ label: 'name', value: 'id' }} + fieldNames={{ label: 'name', value: 'name' }} optionFilterProp="name" showSearch /> - + {/* {resourceType === ResourceType.Dataset && ( -
只允许上传.zip格式文件
+
只允许上传 .zip 格式文件
)}
diff --git a/react-ui/src/pages/Dataset/components/ResourceInfo/index.less b/react-ui/src/pages/Dataset/components/ResourceInfo/index.less new file mode 100644 index 00000000..9da228b4 --- /dev/null +++ b/react-ui/src/pages/Dataset/components/ResourceInfo/index.less @@ -0,0 +1,59 @@ +.resource-info { + height: 100%; + + &__top { + width: 100%; + height: 125px; + margin-bottom: 10px; + padding: 20px 30px; + background-image: url(@/assets/img/dataset-intro-top.png); + background-repeat: no-repeat; + background-position: top center; + background-size: 100% 100%; + + &__name { + margin-right: 10px; + color: @text-color; + font-weight: 500; + font-size: 20px; + } + + &__tag { + padding: 4px 10px; + color: @primary-color; + font-size: 14px; + background: .addAlpha(@primary-color, 0.1) []; + border-radius: 4px; + } + + :global { + .ant-btn-dangerous { + background-color: transparent !important; + } + } + } + + &__bottom { + height: calc(100% - 135px); + padding: 8px 30px 20px; + background: #ffffff; + border-radius: 10px; + box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09); + + :global { + .ant-tabs { + height: 100%; + .ant-tabs-content-holder { + height: 100%; + .ant-tabs-content { + height: 100%; + .ant-tabs-tabpane { + height: 100%; + overflow-y: auto; + } + } + } + } + } + } +} diff --git a/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx b/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx new file mode 100644 index 00000000..9aa752b5 --- /dev/null +++ b/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx @@ -0,0 +1,237 @@ +/* + * @Author: 赵伟 + * @Date: 2024-09-06 09:23:15 + * @Description: 数据集、模型详情 + */ + +import KFIcon from '@/components/KFIcon'; +import { + ResourceData, + ResourceType, + ResourceVersionData, + resourceConfig, +} from '@/pages/Dataset/config'; +import ModelEvolution from '@/pages/Model/components/ModelEvolution'; +import { openAntdModal } from '@/utils/modal'; +import { to } from '@/utils/promise'; +import { getSessionStorageItem, resourceItemKey } from '@/utils/sessionStorage'; +import { modalConfirm } from '@/utils/ui'; +import { useParams, useSearchParams } from '@umijs/max'; +import { App, Button, Flex, Select, Tabs } from 'antd'; +import { pick } from 'lodash'; +import { useEffect, useState } from 'react'; +import AddVersionModal from '../AddVersionModal'; +import ResourceIntro from '../ResourceIntro'; +import ResourceVersion from '../ResourceVersion'; +import styles from './index.less'; + +// 这里值小写是因为值会写在 url 中 +export enum ResourceInfoTabKeys { + Introduction = 'introduction', // 简介 + Version = 'version', // 版本 + Evolution = 'evolution', // 演化 +} + +type ResourceInfoProps = { + resourceType: ResourceType; +}; + +const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { + const [info, setInfo] = useState({} as ResourceData); + const locationParams = useParams(); + const [searchParams] = useSearchParams(); + // 模型演化传入的 tab + const defaultTab = searchParams.get('tab') || ResourceInfoTabKeys.Introduction; + // 模型演化传入的版本 + let versionParam = searchParams.get('version'); + const [versionList, setVersionList] = useState([]); + const [version, setVersion] = useState(undefined); + const [activeTab, setActiveTab] = useState(defaultTab); + const resourceId = Number(locationParams.id); + const config = resourceConfig[resourceType]; + const typeName = config.name; // 数据集/模型 + const { message } = App.useApp(); + + useEffect(() => { + const info = getSessionStorageItem(resourceItemKey, true); + if (info) { + setInfo(info); + getVersionList(pick(info, ['owner', 'identifier'])); + } + }, [resourceId]); + + useEffect(() => { + if (version) { + getResourceDetail({ + ...pick(info, ['owner', 'name', 'id', 'identifier']), + version, + }); + } + }, [version]); + + // 获取详情 + const getResourceDetail = async (params: { + owner: string; + name: string; + id: number; + identifier: string; + version?: string; + }) => { + const request = config.getInfo; + const [res] = await to(request(params)); + if (res) { + setInfo(res.data); + } + }; + + // 获取版本列表 + const getVersionList = async (params: { owner: string; identifier: string }) => { + const request = config.getVersions; + const [res] = await to(request(params)); + if (res && res.data && res.data.length > 0) { + setVersionList(res.data); + if ( + versionParam && + res.data.find((item: ResourceVersionData) => item.name === versionParam) + ) { + setVersion(versionParam); + versionParam = null; + } else { + setVersion(res.data[0].name); + } + } else { + setVersion(undefined); + } + }; + + // 新建版本 + const showModal = () => { + const { close } = openAntdModal(AddVersionModal, { + resourceType: resourceType, + resourceId: resourceId, + resoureName: info.name, + identifier: info.identifier, + onOk: () => { + getVersionList(pick(info, ['owner', 'identifier'])); + close(); + }, + }); + }; + + // 版本变化 + const handleVersionChange = (value: string) => { + setVersion(value); + }; + + // 删除版本 + const deleteVersion = async () => { + const request = config.deleteVersion; + const params = { + identifier: info.identifier, + owner: info.owner, + version, + }; + const [res] = await to(request(params)); + if (res) { + message.success('删除成功'); + setVersion(undefined); + getVersionList(pick(info, ['owner', 'identifier'])); + } + }; + + // 处理删除 + const hanldeDelete = () => { + modalConfirm({ + title: '删除后,该版本将不可恢复', + content: '是否确认删除?', + okText: '确认', + cancelText: '取消', + onOk: () => { + deleteVersion(); + }, + }); + }; + + const items = [ + { + key: ResourceInfoTabKeys.Introduction, + label: `${typeName}简介`, + icon: , + children: , + }, + { + key: ResourceInfoTabKeys.Version, + label: `${typeName}文件`, + icon: , + children: , + }, + ]; + + if (resourceType === ResourceType.Model) { + items.push({ + key: ResourceInfoTabKeys.Evolution, + label: `模型演化`, + icon: , + children: ( + + ), + }); + } + + const infoTypePropertyName = config.infoTypePropertyName as keyof ResourceData; + const infoTagPropertyName = config.infoTagPropertyName as keyof ResourceData; + + return ( +
+
+ +
{info.name}
+ {info[infoTypePropertyName] && ( +
+ {(info[infoTypePropertyName] as string) || '--'} +
+ )} + {info[infoTagPropertyName] && ( +
+ {(info[infoTagPropertyName] as string) || '--'} +
+ )} +
+ + 版本号: + - - - - {!isPublic && ( - - )} -
- {fileList.length > 0 && fileList[0].description - ? '版本描述:' + fileList[0].description - : null} -
); diff --git a/react-ui/src/pages/Dataset/config.tsx b/react-ui/src/pages/Dataset/config.tsx index 822b7bfe..e40aaa4a 100644 --- a/react-ui/src/pages/Dataset/config.tsx +++ b/react-ui/src/pages/Dataset/config.tsx @@ -1,19 +1,17 @@ import KFIcon from '@/components/KFIcon'; import { CommonTabKeys } from '@/enums'; import { - addDatasetVersionDetail, + addDatasetVersion, addModelsVersionDetail, deleteDataset, deleteDatasetVersion, deleteModel, deleteModelVersion, - getDatasetById, + getDatasetInfo, getDatasetList, - getDatasetVersionIdList, - getDatasetVersionsById, + getDatasetVersionList, getModelById, getModelList, - getModelVersionIdList, getModelVersionsById, } from '@/services/dataset/index.js'; import type { TabsProps } from 'antd'; @@ -26,7 +24,6 @@ export enum ResourceType { type ResourceTypeInfo = { getList: (params: any) => Promise; // 获取资源列表 getVersions: (params: any) => Promise; // 获取版本列表 - getFiles: (params: any) => Promise; // 获取版本下的文件列表 deleteRecord: (params: any) => Promise; // 删除 addVersion: (params: any) => Promise; // 新增版本 deleteVersion: (params: any) => Promise; // 删除版本 @@ -55,12 +52,11 @@ type ResourceTypeInfo = { export const resourceConfig: Record = { [ResourceType.Dataset]: { getList: getDatasetList, - getVersions: getDatasetVersionsById, - getFiles: getDatasetVersionIdList, + getVersions: getDatasetVersionList, deleteRecord: deleteDataset, - addVersion: addDatasetVersionDetail, + addVersion: addDatasetVersion, deleteVersion: deleteDatasetVersion, - getInfo: getDatasetById, + getInfo: getDatasetInfo, name: '数据集', typeParamKey: 'data_type', tagParamKey: 'data_tag', @@ -85,17 +81,16 @@ export const resourceConfig: Record = { deleteModalTitle: '确定删除该条数据集实例吗?', addBtnTitle: '新建数据集', idParamKey: 'dataset_id', - uploadAction: '/api/mmp/dataset/upload', + uploadAction: '/api/mmp/newdataset/upload', uploadAccept: '.zip,.tgz', - downloadAllAction: '/api/mmp/dataset/downloadAllFilesl', - downloadSingleAction: '/api/mmp/dataset/download', - infoTypePropertyName: 'dataset_type_name', - infoTagPropertyName: 'dataset_tag_name', + downloadAllAction: '/api/mmp/newdataset/downloadAllFiles', + downloadSingleAction: '/api/mmp/newdataset/downloadSinggerFile', + infoTypePropertyName: 'data_type', + infoTagPropertyName: 'data_tag', }, [ResourceType.Model]: { getList: getModelList, getVersions: getModelVersionsById, - getFiles: getModelVersionIdList, deleteRecord: deleteModel, addVersion: addModelsVersionDetail, deleteVersion: deleteModelVersion, @@ -145,32 +140,37 @@ export type CategoryData = { export type ResourceData = { id: number; name: string; + identifier: string; description: string; create_by: string; + owner: string; update_time: string; - available_range: number; + time_ago: string; + is_public: boolean; model_type_name?: string; model_tag_name?: string; - dataset_type_name?: string; - dataset_tag_name?: string; + data_type?: string; + data_tag?: string; + version?: string; + version_desc?: string; + processing_code?: string; + dataset_source?: string; + usage?: string; + dataset_version_vos: ResourceFileData[]; }; // 版本数据 export type ResourceVersionData = { - label: string; - value: string; + name: string; + http_url: string; + tar_url: string; + zip_url: string; }; // 版本文件数据 export type ResourceFileData = { - id: number; file_name: string; file_size: string; - description: string; - create_by: string; - create_time: string; - update_by: string; - update_time: string; url: string; - version: string; + update_time?: string; }; diff --git a/react-ui/src/pages/Dataset/intro.tsx b/react-ui/src/pages/Dataset/intro.tsx index 6a9e14ec..e40f8288 100644 --- a/react-ui/src/pages/Dataset/intro.tsx +++ b/react-ui/src/pages/Dataset/intro.tsx @@ -1,8 +1,8 @@ -import ResourceIntro from '@/pages/Dataset/components/ResourceIntro'; +import ResourceInfo from '@/pages/Dataset/components/ResourceInfo'; import { ResourceType } from '@/pages/Dataset/config'; -function DatasetIntro() { - return ; +function DatasetInfo() { + return ; } -export default DatasetIntro; +export default DatasetInfo; diff --git a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx index 4278a358..73a2006f 100644 --- a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx +++ b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx @@ -8,11 +8,11 @@ import KFRadio, { type KFRadioItem } from '@/components/KFRadio'; import PageTitle from '@/components/PageTitle'; import ResourceSelect, { requiredValidator, + ResourceSelectorType, type ParameterInputObject, } from '@/components/ResourceSelect'; import SubAreaTitle from '@/components/SubAreaTitle'; import { useComputingResource } from '@/hooks/resource'; -import { ResourceSelectorType } from '@/pages/Pipeline/components/ResourceSelectorModal'; import { createEditorReq } from '@/services/developmentEnvironment'; import { to } from '@/utils/promise'; import { useNavigate } from '@umijs/max'; @@ -90,7 +90,6 @@ function EditorCreate() { { const type = item.item_type; + if (type === 'code') { + return ; + } + let selectorType: ResourceSelectorType; if (type === 'dataset') { selectorType = ResourceSelectorType.Dataset; diff --git a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx index 01e30ab0..c1619ad0 100644 --- a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx +++ b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx @@ -1,17 +1,21 @@ import datasetImg from '@/assets/img/modal-select-dataset.png'; import mirrorImg from '@/assets/img/modal-select-mirror.png'; import modelImg from '@/assets/img/modal-select-model.png'; -import { CommonTabKeys, MirrorVersionStatus } from '@/enums'; +import { AvailableRange, CommonTabKeys } from '@/enums'; +import { ResourceData, ResourceVersionData } from '@/pages/Dataset/config'; +import { MirrorVersionData } from '@/pages/Mirror/Info'; +import { MirrorData } from '@/pages/Mirror/List'; import { + getDatasetInfo, getDatasetList, - getDatasetVersionIdList, - getDatasetVersionsById, + getDatasetVersionList, getModelList, getModelVersionIdList, getModelVersionsById, } from '@/services/dataset/index.js'; import { getMirrorListReq, getMirrorVersionListReq } from '@/services/mirror'; -import type { TabsProps } from 'antd'; +import type { TabsProps, TreeDataNode } from 'antd'; +import { pick } from 'lodash'; export enum ResourceSelectorType { Model = 'Model', // 模型 @@ -19,111 +23,347 @@ export enum ResourceSelectorType { Mirror = 'Mirror', //镜像 } -export type MirrorVersion = { - id: number; // 镜像版本 id - status: MirrorVersionStatus; // 镜像版本状态 - tag_name: string; // 镜像版本 name - url: string; // 镜像版本路径 +// 数据集、模型列表转为树形结构 +const convertDatasetToTreeData = (list: ResourceData[]): TreeDataNode[] => { + return list.map((v) => ({ + ...v, + key: `${v.id}`, + title: v.name, + isLeaf: false, + checkable: false, + })); }; -export type SelectorTypeInfo = { - getList: (params: any) => Promise; // 获取资源列表 - getVersions: (params: any) => Promise; // 获取资源版本列表 - getFiles: (params: any) => Promise; // 获取资源文件列表 - handleVersionResponse: (res: any) => any[]; // 处理版本列表接口数据 - modalIcon: string; // modal icon - buttonIcon: string; // button icon - name: string; // 名称 - litReqParamKey: 'available_range' | 'image_type'; // 表示是公开还是私有的参数名称,获取资源列表接口使用 - fileReqParamKey: 'models_id' | 'dataset_id'; // 文件请求参数名称,获取文件列表接口使用 - tabItems: TabsProps['items']; // tab 列表 - buttontTitle: string; // 按钮 title +// 镜像列表转为树形结构 +const convertMirrorToTreeData = (list: MirrorData[]): TreeDataNode[] => { + return list.map((v) => ({ + key: `${v.id}`, + title: v.name, + isLeaf: false, + checkable: false, + })); }; -// 获取镜像文件列表,为了兼容数据集和模型 -const getMirrorFilesReq = ({ id, version }: { id: number; version: string }): Promise => { - const index = version.indexOf('-'); - const url = version.slice(index + 1); - return Promise.resolve({ - data: { +// 数据集版本列表转为树形结构 +const convertDatasetVersionToTreeData = ( + parentId: string, + info: ResourceData, + list: ResourceVersionData[], +): TreeDataNode[] => { + return list.map((item: ResourceVersionData) => ({ + ...pick(info, ['id', 'name', 'owner', 'identifier']), + version: item.name, + title: item.name, + key: `${parentId}-${item.name}`, + isLeaf: true, + checkable: true, + })); +}; + +// 镜像版本列表转为树形结构 +const convertMirrorVersionToTreeData = ( + parentId: string, + list: MirrorVersionData[], +): TreeDataNode[] => { + return list.map((item: MirrorVersionData) => ({ + url: item.url, + title: item.tag_name, + key: `${parentId}-${item.id}`, + isLeaf: true, + checkable: true, + })); +}; + +// 从树形数据节点 id 中获取数据集版本列表的参数 +// const parseDatasetVersionId = (id: string) => { +// const list = id.split('-'); +// return { +// id: Number(list[0]), +// name: list[1], +// owner: list[2], +// identifier: list[3], +// version: list[4], +// }; +// }; + +// 从树形数据节点 id 中获取数据集版本列表的参数 +// const parseMirrorVersionId = (id: string) => { +// const list = id.split('-'); +// return { +// parentId: Number(list[0]), +// id: list[1], +// url: list[2], +// }; +// }; + +// export type MirrorVersion = { +// id: number; // 镜像版本 id +// status: MirrorVersionStatus; // 镜像版本状态 +// tag_name: string; // 镜像版本 name +// url: string; // 镜像版本路径 +// }; + +// export type SelectorTypeInfo = { +// getList: (params: any) => Promise; // 获取资源列表 +// getVersions: (params: any) => Promise; // 获取资源版本列表 +// getFiles: (params: any) => Promise; // 获取资源文件列表 +// handleVersionResponse: (res: any) => any[]; // 处理版本列表接口数据 +// dataToTreeData: (data: any) => TreeDataNode[]; // 数据转树形结构 +// parseTreeNodeId: (id: string) => any; // 获取版本列表请求参数 +// modalIcon: string; // modal icon +// buttonIcon: string; // button icon +// name: string; // 名称 +// litReqParamKey: 'available_range' | 'image_type'; // 表示是公开还是私有的参数名称,获取资源列表接口使用 +// tabItems: TabsProps['items']; // tab 列表 +// buttontTitle: string; // 按钮 title +// }; + +// export const selectorTypeConfig: Record = { +// [ResourceSelectorType.Model]: { +// getList: getModelList, +// getVersions: getModelVersionsById, +// getFiles: getModelVersionIdList, + +// name: '模型', +// modalIcon: modelImg, +// buttonIcon: 'icon-xuanzemoxing', +// litReqParamKey: 'available_range', +// tabItems: [ +// { +// key: CommonTabKeys.Private, +// label: '我的模型', +// }, +// { +// key: CommonTabKeys.Public, +// label: '公开模型', +// }, +// ], +// buttontTitle: '选择模型', +// }, +// [ResourceSelectorType.Dataset]: { +// getList: getDatasetList, +// getVersions: getDatasetVersionList, +// getFiles: getDatasetInfo, + +// name: '数据集', +// modalIcon: datasetImg, +// buttonIcon: 'icon-xuanzeshujuji', +// litReqParamKey: 'available_range', +// tabItems: [ +// { +// key: CommonTabKeys.Private, +// label: '我的数据集', +// }, +// { +// key: CommonTabKeys.Public, +// label: '公开数据集', +// }, +// ], +// buttontTitle: '选择数据集', +// }, +// [ResourceSelectorType.Mirror]: { +// getList: getMirrorListReq, +// getVersions: (id: number) => getMirrorVersionListReq({ image_id: id, page: 0, size: 200 }), +// getFiles: getMirrorFilesReq, +// handleVersionResponse: (res) => +// res.data?.content?.filter( +// (v: MirrorVersionData) => v.status === MirrorVersionStatus.Available, +// ) || [], +// dataToTreeData: convertMirrorToTreeData, +// parseTreeNodeId: (id: string) => id, +// name: '镜像', +// modalIcon: mirrorImg, +// buttonIcon: 'icon-xuanzejingxiang', +// litReqParamKey: 'image_type', +// tabItems: [ +// { +// key: CommonTabKeys.Private, +// label: '我的镜像', +// }, +// { +// key: CommonTabKeys.Public, +// label: '公开镜像', +// }, +// ], +// buttontTitle: '选择镜像', +// }, +// }; + +interface SelectorTypeInfo { + getList: (isPublic: boolean) => Promise; // 获取资源列表 + getVersions: (parentKey: string, parentNode: any) => Promise; // 获取资源版本列表 + getFiles: (parentKey: string, parentNode: any) => Promise; // 获取资源文件列表 + readonly modalIcon: string; // modal icon + readonly buttonIcon: string; // button icon + readonly name: string; // 名称 + readonly tabItems: TabsProps['items']; // tab 列表 + readonly buttontTitle: string; // 按钮 title +} + +export class DatasetSelector implements SelectorTypeInfo { + readonly name = '数据集'; + readonly modalIcon = datasetImg; + readonly buttonIcon = 'icon-xuanzeshujuji'; + readonly tabItems = [ + { + key: CommonTabKeys.Private, + label: '我的数据集', + }, + { + key: CommonTabKeys.Public, + label: '公开数据集', + }, + ]; + readonly buttontTitle = '选择数据集'; + + async getList(isPublic: boolean) { + const res = await getDatasetList({ is_public: isPublic, page: 0, size: 2000 }); + if (res && res.data) { + const list = res.data.content || []; + return convertDatasetToTreeData(list); + } else { + return Promise.reject('获取数据集列表失败'); + } + } + async getVersions(parentKey: string, parentNode: ResourceData) { + // const obj = parseDatasetVersionId(id); + const res = await getDatasetVersionList(pick(parentNode, ['owner', 'identifier'])); + if (res && res.data) { + const list = res.data; + return convertDatasetVersionToTreeData(parentKey, parentNode, list); + } else { + return Promise.reject('获取数据集版本列表失败'); + } + } + + async getFiles(_parentKey: string, parentNode: ResourceData & ResourceVersionData) { + //const obj = parseDatasetVersionId(parentKey); + const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version']); + const res = await getDatasetInfo(params); + if (res && res.data) { + const path = res.data.relative_paths || ''; + const list = res.data.dataset_version_vos || []; + return { + path, + content: list, + }; + } else { + return Promise.reject('获取数据集文件列表失败'); + } + } +} + +export class ModelSelector implements SelectorTypeInfo { + readonly name = '模型'; + readonly modalIcon = modelImg; + readonly buttonIcon = 'icon-xuanzemoxing'; + readonly tabItems = [ + { + key: CommonTabKeys.Private, + label: '我的模型', + }, + { + key: CommonTabKeys.Public, + label: '公开模型', + }, + ]; + readonly buttontTitle = '选择模型'; + + async getList(isPublic: boolean) { + const res = await getModelList({ is_public: isPublic, page: 0, size: 2000 }); + if (res && res.data) { + const list = res.data.content || []; + return convertDatasetToTreeData(list); + } else { + return Promise.reject('获取数据集列表失败'); + } + } + async getVersions(key: string, parentNode: ResourceData) { + //const obj = parseDatasetVersionId(id); + const res = await getModelVersionIdList(pick(parentNode, ['owner', 'identifier'])); + if (res && res.data) { + const list = res.data.content || []; + return convertDatasetVersionToTreeData(key, parentNode, list); + } else { + return Promise.reject('获取数据集版本列表失败'); + } + } + + async getFiles(_parentKey: string, parentNode: ResourceData & ResourceVersionData) { + // const obj = parseDatasetVersionId(id); + const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version']); + const res = await getModelVersionsById(params); + if (res && res.data) { + const list = res.data.dataset_version_vos || []; + return { + path: res.data.path || '', + content: list, + }; + } else { + return Promise.reject('获取数据集文件列表失败'); + } + } +} + +export class MirrorSelector implements SelectorTypeInfo { + readonly name = '镜像'; + readonly modalIcon = mirrorImg; + readonly buttonIcon = 'icon-xuanzejingxiang'; + readonly tabItems = [ + { + key: CommonTabKeys.Private, + label: '我的镜像', + }, + { + key: CommonTabKeys.Public, + label: '公开镜像', + }, + ]; + readonly buttontTitle = '选择镜像'; + + async getList(isPublic: boolean) { + const res = await getMirrorListReq({ + image_type: isPublic ? AvailableRange.Public : AvailableRange.Private, + page: 0, + size: 2000, + }); + if (res && res.data) { + const list = res.data.content || []; + return convertMirrorToTreeData(list); + } else { + return Promise.reject('获取镜像列表失败'); + } + } + async getVersions(parentKey: string) { + const res = await getMirrorVersionListReq({ + image_id: parentKey, + page: 0, + size: 2000, + }); + if (res && res.data) { + const list = res.data.content || []; + return convertMirrorVersionToTreeData(parentKey, list); + } else { + return Promise.reject('获取镜像版本列表失败'); + } + } + + async getFiles(parentKey: string, parentNode: MirrorVersionData) { + const { url } = parentNode; + return { path: url, content: [ { - id: `${id}-${version}`, + id: parentKey, file_name: `${url}`, }, ], - }, - }); -}; + }; + } +} export const selectorTypeConfig: Record = { - [ResourceSelectorType.Model]: { - getList: getModelList, - getVersions: getModelVersionsById, - getFiles: getModelVersionIdList, - handleVersionResponse: (res) => res.data || [], - name: '模型', - modalIcon: modelImg, - buttonIcon: 'icon-xuanzemoxing', - litReqParamKey: 'available_range', - fileReqParamKey: 'models_id', - tabItems: [ - { - key: CommonTabKeys.Private, - label: '我的模型', - }, - { - key: CommonTabKeys.Public, - label: '公开模型', - }, - ], - buttontTitle: '选择模型', - }, - [ResourceSelectorType.Dataset]: { - getList: getDatasetList, - getVersions: getDatasetVersionsById, - getFiles: getDatasetVersionIdList, - handleVersionResponse: (res) => res.data || [], - name: '数据集', - modalIcon: datasetImg, - buttonIcon: 'icon-xuanzeshujuji', - litReqParamKey: 'available_range', - fileReqParamKey: 'dataset_id', - tabItems: [ - { - key: CommonTabKeys.Private, - label: '我的数据集', - }, - { - key: CommonTabKeys.Public, - label: '公开数据集', - }, - ], - buttontTitle: '选择数据集', - }, - [ResourceSelectorType.Mirror]: { - getList: getMirrorListReq, - getVersions: (id: number) => getMirrorVersionListReq({ image_id: id, page: 0, size: 200 }), - getFiles: getMirrorFilesReq, - handleVersionResponse: (res) => - res.data?.content?.filter((v: MirrorVersion) => v.status === MirrorVersionStatus.Available) || - [], - name: '镜像', - modalIcon: mirrorImg, - buttonIcon: 'icon-xuanzejingxiang', - litReqParamKey: 'image_type', - fileReqParamKey: 'dataset_id', - tabItems: [ - { - key: CommonTabKeys.Private, - label: '我的镜像', - }, - { - key: CommonTabKeys.Public, - label: '公开镜像', - }, - ], - buttontTitle: '选择镜像', - }, + [ResourceSelectorType.Model]: new ModelSelector(), + [ResourceSelectorType.Dataset]: new DatasetSelector(), + [ResourceSelectorType.Mirror]: new MirrorSelector(), }; diff --git a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.less b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.less index bfff8100..64aaad64 100644 --- a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.less +++ b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.less @@ -67,3 +67,8 @@ } } } + +.kf-tree-title { + display: inline-block; + .singleLine(); +} diff --git a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx index f4caf03a..4c7d33b9 100644 --- a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx +++ b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx @@ -11,24 +11,19 @@ import { Icon } from '@umijs/max'; import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd'; import { Input, Tabs, Tree } from 'antd'; import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { MirrorVersion, ResourceSelectorType, selectorTypeConfig } from './config'; +import { ResourceSelectorType, selectorTypeConfig } from './config'; import styles from './index.less'; export { ResourceSelectorType, selectorTypeConfig }; // 选择数据集\模型\镜像的返回类型 export type ResourceSelectorResponse = { - id: number; // 数据集\模型\镜像 id + id: string; // 数据集\模型\镜像 id name: string; // 数据集\模型\镜像 name version: string; // 数据集\模型\镜像版本 path: string; // 数据集\模型\镜像版本路径 activeTab: CommonTabKeys; // 是我的还是公开的 }; -type ResourceGroup = { - id: number; // 数据集\模型\镜像 id - name: string; // 数据集\模型\镜像 name -}; - type ResourceFile = { id: number; // 文件 id file_name: string; // 文件 name @@ -44,39 +39,8 @@ export interface ResourceSelectorModalProps extends Omit { type TreeRef = GetRef>; -// list 数据转成 treeData -const convertToTreeData = (list: ResourceGroup[]): TreeDataNode[] => { - return list.map((v) => ({ - title: v.name, - key: v.id, - isLeaf: false, - checkable: false, - })); -}; - -// 版本数据转成 treeData -const convertVersionToTreeData = (parentId: number) => { - return (item: string | MirrorVersion): TreeDataNode => { - if (typeof item === 'string') { - return { - title: item, - key: `${parentId}-${item}`, - isLeaf: true, - checkable: true, - }; - } else { - return { - title: item.tag_name, - key: `${parentId}-${item.id}-${item.url}`, - isLeaf: true, - checkable: true, - }; - } - }; -}; - // 更新树形结构的 children -const updateChildren = (parentId: number, children: TreeDataNode[]) => { +const updateChildren = (parentId: string, children: TreeDataNode[]) => { return (node: TreeDataNode) => { if (node.key === parentId) { return { @@ -91,7 +55,7 @@ const updateChildren = (parentId: number, children: TreeDataNode[]) => { // 得到数据集\模型\镜像 id 和下属版本号 const getIdAndVersion = (versionKey: string) => { const index = versionKey.indexOf('-'); - const id = Number(versionKey.slice(0, index)); + const id = versionKey.slice(0, index); const version = versionKey.slice(index + 1); return { id, @@ -115,8 +79,8 @@ function ResourceSelectorModal({ const [files, setFiles] = useState([]); const [versionPath, setVersionPath] = useState(''); const [searchText, setSearchText] = useState(''); - const [fisrtLoadList, setFisrtLoadList] = useState(false); - const [fisrtLoadVersions, setFisrtLoadVersions] = useState(false); + const [firstLoadList, setFirstLoadList] = useState(false); + const [firstLoadVersions, setFirstLoadVersions] = useState(false); const treeRef = useRef(null); const config = selectorTypeConfig[type]; @@ -140,18 +104,10 @@ function ResourceSelectorModal({ // 获取数据集\模型\镜像列表 const getTreeData = async () => { - const available_range = activeTab === CommonTabKeys.Private ? 0 : 1; - const params = { - page: 0, - size: 1000, - [config.litReqParamKey]: available_range, - }; - const getListReq = config.getList; - const [res] = await to(getListReq(params)); + const isPublic = activeTab === CommonTabKeys.Private ? false : true; + const [res] = await to(config.getList(isPublic)); if (res) { - const list = res.data?.content || []; - const treeData = convertToTreeData(list); - setOriginTreeData(treeData); + setOriginTreeData(res); // 恢复上一次的 Expand 操作 restoreLastExpand(); @@ -161,21 +117,22 @@ function ResourceSelectorModal({ }; // 获取数据集\模型\镜像版本列表 - const getVersions = async (parentId: number) => { - const getVersionsReq = config.getVersions; - const [res, error] = await to(getVersionsReq(parentId)); + const getVersions = async (parentId: string, parentNode: any) => { + const [res, error] = await to(config.getVersions(parentId, parentNode)); if (res) { - const list = config.handleVersionResponse(res); - const children = list.map(convertVersionToTreeData(parentId)); // 更新 treeData children - setOriginTreeData((prev) => prev.map(updateChildren(parentId, children))); + setOriginTreeData((prev) => prev.map(updateChildren(parentId, res))); + // 缓存 loadedKeys const index = loadedKeys.find((v) => v === parentId); if (!index) { setLoadedKeys((prev) => prev.concat(parentId)); } + // 恢复上一次的 Check 操作 - restoreLastCheck(parentId); + setTimeout(() => { + restoreLastCheck(parentId, res); + }, 300); } else { setExpandedKeys([]); return Promise.reject(error); @@ -183,14 +140,11 @@ function ResourceSelectorModal({ }; // 获取版本下的文件 - const getFiles = async (id: number, version: string) => { - const getFilesReq = config.getFiles; - const paramsKey = config.fileReqParamKey; - const params = { version: version, [paramsKey]: id }; - const [res] = await to(getFilesReq(params)); + const getFiles = async (parentId: string, parentNode: any) => { + const [res] = await to(config.getFiles(parentId, parentNode)); if (res) { - setVersionPath(res.data?.path || ''); - setFiles(res.data?.content || []); + setVersionPath(res.path); + setFiles(res.content); } else { setVersionPath(''); setFiles([]); @@ -198,11 +152,11 @@ function ResourceSelectorModal({ }; // 动态加载 tree children - const onLoadData = ({ key, children }: TreeDataNode) => { + const onLoadData = ({ key, children, ...rest }: TreeDataNode) => { if (children) { return Promise.resolve(); } else { - return getVersions(key as number); + return getVersions(key as string, rest); } }; @@ -213,13 +167,13 @@ function ResourceSelectorModal({ }; // 选中 - const onCheck: TreeProps['onCheck'] = (checkedKeysValue) => { + const onCheck: TreeProps['onCheck'] = (checkedKeysValue, { checkedNodes }) => { const lastKeys = (checkedKeysValue as React.Key[]).slice(-1); setCheckedKeys(lastKeys); - if (lastKeys.length) { + if (lastKeys.length && checkedNodes.length) { const last = lastKeys[0] as string; - const { id, version } = getIdAndVersion(last); - getFiles(id, version); + const lastNode = checkedNodes[checkedNodes.length - 1]; + getFiles(last, lastNode); } else { setFiles([]); } @@ -229,10 +183,10 @@ function ResourceSelectorModal({ // 判断是否有 defaultExpandedKeys,如果有,设置 expandedKeys // fisrtLoadList 标志位 const restoreLastExpand = () => { - if (!fisrtLoadList && defaultExpandedKeys.length > 0) { + if (!firstLoadList && defaultExpandedKeys.length > 0) { setTimeout(() => { setExpandedKeys(defaultExpandedKeys); - setFisrtLoadList(true); + setFirstLoadList(true); setTimeout(() => { treeRef.current?.scrollTo({ key: defaultExpandedKeys[0], align: 'bottom' }); }, 100); @@ -243,16 +197,17 @@ function ResourceSelectorModal({ // 恢复上一次的 Check 操作 // 判断是否有 defaultCheckedKeys,如果有,设置 checkedKeys,并且调用获取文件列表接口 // fisrtLoadVersions 标志位 - const restoreLastCheck = (parentId: number) => { - if (!fisrtLoadVersions && defaultCheckedKeys.length > 0) { + const restoreLastCheck = (parentId: string, versions: TreeDataNode[]) => { + if (!firstLoadVersions && defaultCheckedKeys.length > 0) { const last = defaultCheckedKeys[0] as string; - const { id, version } = getIdAndVersion(last); + const { id } = getIdAndVersion(last); // 判断正在打开的 id 和 defaultCheckedKeys 的 id 是否一致 if (id === parentId) { setTimeout(() => { setCheckedKeys(defaultCheckedKeys); - getFiles(id, version); - setFisrtLoadVersions(true); + const parentNode = versions.find((v) => v.key === last); + getFiles(last, parentNode); + setFirstLoadVersions(true); setTimeout(() => { treeRef?.current?.scrollTo({ key: defaultCheckedKeys[0], @@ -269,7 +224,7 @@ function ResourceSelectorModal({ 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 name = (treeData.find((v) => v.key === id)?.title ?? '') as string; const res = { id, name, @@ -323,6 +278,18 @@ function ResourceSelectorModal({ loadedKeys={loadedKeys} expandedKeys={expandedKeys} onExpand={onExpand} + titleRender={(nodeData) => { + console.log(nodeData); + + return ( + + {nodeData.title as string} + + ); + }} checkable /> diff --git a/react-ui/src/pages/System/User/edit.tsx b/react-ui/src/pages/System/User/edit.tsx index 324d5df0..4fde814d 100644 --- a/react-ui/src/pages/System/User/edit.tsx +++ b/react-ui/src/pages/System/User/edit.tsx @@ -62,6 +62,8 @@ const UserForm: React.FC = (props) => { loginIp: props.values.loginIp, loginDate: props.values.loginDate, remark: props.values.remark, + gitLinkUsername: props.values.gitLinkUsername, + gitLinkPassword: props.values.gitLinkPassword, }); }, [form, props]); @@ -275,6 +277,28 @@ const UserForm: React.FC = (props) => { colProps={{ md: 12, xl: 12 }} rules={[{ required: true, message: '请选择角色!' }]} /> + + ( -
- - 页面开发中,敬请期待...... -
-); +const MissingPage = () => { + const navigate = useNavigate(); + + return ( + navigate('/')} + > + ); +}; export default MissingPage; diff --git a/react-ui/src/requestConfig.ts b/react-ui/src/requestConfig.ts index 01911926..5e02e678 100644 --- a/react-ui/src/requestConfig.ts +++ b/react-ui/src/requestConfig.ts @@ -7,6 +7,7 @@ import type { AxiosRequestConfig, AxiosResponse, RequestConfig, RequestOptions } import { message } from 'antd'; import { clearSessionToken, getAccessToken } from './access'; import { setRemoteMenu } from './services/session'; +import Loading from './utils/loading'; import { gotoLoginPage } from './utils/ui'; // [antd: Notification] You are calling notice in render which will break in React 18 concurrent mode. Please trigger in effect instead. @@ -36,12 +37,14 @@ export const requestConfig: RequestConfig = { headers['Authorization'] = `Bearer ${accessToken}`; } } + Loading.show(); return { url, options }; }, ], responseInterceptors: [ [ (response: AxiosResponse) => { + Loading.hide(); const { status, data, config } = response || {}; const skipErrorHandler = (config as RequestOptions)?.skipErrorHandler; if (status >= 200 && status < 300) { @@ -63,6 +66,7 @@ export const requestConfig: RequestConfig = { } }, (error: Error) => { + Loading.hide(); popupError(error.message ?? '请求失败'); return Promise.reject(error); }, diff --git a/react-ui/src/services/dataset/index.js b/react-ui/src/services/dataset/index.js index 1764c8d0..dec43db8 100644 --- a/react-ui/src/services/dataset/index.js +++ b/react-ui/src/services/dataset/index.js @@ -1,21 +1,34 @@ import { request } from '@umijs/max'; -// 分页查询数据集 + +// 查询数据集、模型分类 +export function getAssetIcon(params) { + return request(`/api/mmp/assetIcon`, { + method: 'GET', + params, + }); +} + +// ----------------------------数据集--------------------------------- + +// 分页查询数据集列表 export function getDatasetList(params) { - return request(`/api/mmp/dataset`, { + return request(`/api/mmp/newdataset/queryDatasets`, { method: 'GET', params, }); } -// 分页查询模型 -export function getModelList(params) { - return request(`/api/mmp/models`, { + +// 查询数据集详情 +export function getDatasetInfo(params) { + return request(`/api/mmp/newdataset/getDatasetDetail`, { method: 'GET', params, }); } + // 新增数据集 -export function addDatesetAndVesion(data) { - return request(`/api/mmp/dataset/addDatasetAndVersion`, { +export function addDateset(data) { + return request(`/api/mmp/newdataset/addDatasetAndVersion`, { method: 'POST', headers: { 'Content-Type': 'application/json;charset=UTF-8', @@ -23,9 +36,35 @@ export function addDatesetAndVesion(data) { data, }); } -// 新增模型 -export function addModel(data) { - return request(`/api/mmp/models/addModelAndVersion`, { + +// 删除数据集 +export function deleteDataset(params) { + return request(`/api/mmp/newdataset/deleteDataset`, { + method: 'DELETE', + params, + }); +} + + +// 查询数据集版本列表 +export function getDatasetVersionList(params) { + return request(`/api/mmp/newdataset/getVersionList`, { + method: 'GET', + params, + }); +} + +// 查询数据集版本文件列表 +// export function getDatasetVersionFiles(params) { +// return request(`/api/mmp/datasetVersion/versions`, { +// method: 'GET', +// params, +// }); +// } + +// 新增数据集版本 +export function addDatasetVersion(data) { + return request(`/api/mmp/newdataset/addVersion`, { method: 'POST', headers: { 'Content-Type': 'application/json;charset=UTF-8', @@ -33,75 +72,74 @@ export function addModel(data) { data, }); } -// 查询数据集简介 -export function getDatasetById(id) { - return request(`/api/mmp/dataset/${id}`, { + +// 下载数据集所有文件 +export function downloadAllFiles(params) { + return request(`/api/mmp/newdataset/downloadAllFiles`, { method: 'GET', + params }); } -// 查询左侧列表 -export function getAssetIcon(params) { - return request(`/api/mmp/assetIcon`, { + +// 下载数据集单个文件 +export function downloadSingleFile(params) { + return request(`/api/mmp/newdataset/downloadSinggerFile`, { method: 'GET', params, }); } -// 查询模型简介 -export function getModelById(id) { - return request(`/api/mmp/models/${id}`, { - method: 'GET', + +// 删除数据集版本 +export function deleteDatasetVersion(params) { + return request(`/api/mmp/newdataset/deleteDatasetVersion`, { + method: 'DELETE', + params, }); } -// 查询数据版本集 -export function getDatasetVersionsById(id) { - return request(`/api/mmp/dataset/versions/${id}`, { + +// ----------------------------模型--------------------------------- + +// 分页查询模型列表 +export function getModelList(params) { + return request(`/api/mmp/models`, { method: 'GET', + params, }); } -// 查询模型版本集 -export function getModelVersionsById(id) { - return request(`/api/mmp/models/versions/${id}`, { + +// 新增模型 +export function addModel(data) { + return request(`/api/mmp/models/addModelAndVersion`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json;charset=UTF-8', + }, + data, + }); +} + +// 查询模型简介 +export function getModelById(id) { + return request(`/api/mmp/models/${id}`, { method: 'GET', }); } -// 分页查询数据集 -export function getDatasetVersionIdList(params) { - return request(`/api/mmp/datasetVersion/versions`, { + +// 查询模型版本列表 +export function getModelVersionsById(id) { + return request(`/api/mmp/models/versions/${id}`, { method: 'GET', - params, }); } -// 根据版本查询模型 + +// 根据版本查询文件列表 export function getModelVersionIdList(params) { return request(`/api/mmp/modelsVersion/versions`, { method: 'GET', params, }); } -// 删除数据集 -export function deleteDatasetVersion(params) { - return request(`/api/mmp/datasetVersion/deleteVersion`, { - method: 'DELETE', - params, - }); -} -// 删除模型 -export function deleteModelVersion(params) { - return request(`/api/mmp/modelsVersion/deleteVersion`, { - method: 'DELETE', - params, - }); -} -// 新增数据集版本 -export function addDatasetVersionDetail(data) { - return request(`/api/mmp/datasetVersion/addDatasetVersions`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json;charset=UTF-8', - }, - data, - }); -} + // 新增模型版本 export function addModelsVersionDetail(data) { return request(`/api/mmp/modelsVersion/addModelVersions`, { @@ -112,24 +150,22 @@ export function addModelsVersionDetail(data) { data, }); } -// 下载数据集 -export function exportDataset(id) { - return request(`/api/mmp/dataset/download/${id}`, { - method: 'GET', - }); -} -// 删除模型集 + +// 删除模型 export function deleteModel(id) { return request(`/api/mmp/models/${id}`, { method: 'DELETE', }); } -// 删除数据集 -export function deleteDataset(id) { - return request(`/api/mmp/dataset/${id}`, { + +// 删除模型版本 +export function deleteModelVersion(params) { + return request(`/api/mmp/modelsVersion/deleteVersion`, { method: 'DELETE', + params, }); } + // 获取模型依赖 export function getModelAtlasReq(data) { return request(`/api/mmp/modelDependency/queryModelAtlas`, { diff --git a/react-ui/src/types/system/user.d.ts b/react-ui/src/types/system/user.d.ts index 5f0ba1c2..a74ee0e8 100644 --- a/react-ui/src/types/system/user.d.ts +++ b/react-ui/src/types/system/user.d.ts @@ -19,6 +19,8 @@ declare namespace API.System { updateBy: string; updateTime: Date; remark: string; + gitLinkUsername?: string; + gitLinkPassword?: string; } export interface UserListParams { diff --git a/react-ui/src/utils/loading.tsx b/react-ui/src/utils/loading.tsx index aba4a16c..8486a6b9 100644 --- a/react-ui/src/utils/loading.tsx +++ b/react-ui/src/utils/loading.tsx @@ -11,29 +11,37 @@ import zhCN from 'antd/locale/zh_CN'; import { createRoot } from 'react-dom/client'; export class Loading { - static total = 0; + static total: number = 0; + static isShowing: boolean = false; + static startTime: Date = new Date(); + static removeTimeout: ReturnType | undefined; static show(props?: SpinProps) { - Loading.total += 1; - if (Loading.total > 1) { + this.total += 1; + if (this.total > 1) { + return; + } + + // 是否有延时未关闭的 loading + if (this.isShowing) { + this.clearRemoveTimeout(); return; } const container = document.createElement('div'); container.id = 'loading'; - const rootContainer = document.getElementsByTagName('main')[0]; + const rootContainer = document.body; //document.getElementsByTagName('main')[0]; rootContainer?.appendChild(container); const root = createRoot(container); const global = globalConfig(); - let timeoutId: ReturnType; + let renderTimeoutId: ReturnType; - function render(spinProps: SpinProps) { - clearTimeout(timeoutId); + const render = (spinProps: SpinProps) => { + clearTimeout(renderTimeoutId); - timeoutId = setTimeout(() => { + renderTimeoutId = setTimeout(() => { const rootPrefixCls = global.getPrefixCls(); const iconPrefixCls = global.getIconPrefixCls(); const theme = global.getTheme(); const dom = ; - root.render( , ); }); - } + }; render({ size: 'large', ...props, spinning: true }); + this.startTime = new Date(); + this.isShowing = true; + } + + static clearRemoveTimeout() { + if (this.removeTimeout) { + clearTimeout(this.removeTimeout); + this.removeTimeout = undefined; + } + } + + static removeLoading() { + this.clearRemoveTimeout(); + const rootContainer = document.body; //document.getElementsByTagName('main')[0]; + const container = document.getElementById('loading'); + if (container) { + rootContainer?.removeChild(container); + } + this.isShowing = false; } static hide(force: boolean = false) { - Loading.total -= 1; - if (Loading.total <= 0 || force) { - Loading.total = 0; - const rootContainer = document.getElementsByTagName('main')[0]; - const container = document.getElementById('loading'); - if (container) { - rootContainer?.removeChild(container); - } + this.total -= 1; + if (this.total > 0 && !force) { + return; } + + this.total = 0; + const duration = new Date().getTime() - this.startTime.getTime(); + this.removeTimeout = setTimeout(() => { + this.removeLoading(); + }, Math.max(300 - duration, 0)); } } diff --git a/react-ui/src/utils/sessionStorage.ts b/react-ui/src/utils/sessionStorage.ts index 00b2e313..dbb4de4f 100644 --- a/react-ui/src/utils/sessionStorage.ts +++ b/react-ui/src/utils/sessionStorage.ts @@ -4,8 +4,20 @@ export const mirrorNameKey = 'mirror-name'; export const modelDeploymentInfoKey = 'model-deployment-info'; // 编辑器 url export const editorUrlKey = 'editor-url'; +// 数据集、模型资源 +export const resourceItemKey = 'resource-item'; -export const getSessionStorageItem = (key: string, isObject: boolean = false) => { +/** + * Retrieves an item from session storage by key. + * + * If `isObject` is true, the function attempts to parse the stored value as JSON. + * If parsing fails, the function returns undefined. + * + * @param {string} key - The key of the item to retrieve + * @param {boolean} [isObject=false] - Whether to parse the stored value as JSON + * @return {any} The retrieved item, or undefined if not found or parsing fails + */ +export const getSessionStorageItem = (key: string, isObject: boolean = false): any => { const jsonStr = sessionStorage.getItem(key); if (!isObject) { return jsonStr; @@ -20,18 +32,40 @@ export const getSessionStorageItem = (key: string, isObject: boolean = false) => return undefined; }; +/** + * Sets an item in session storage by key. + * + * If `isObject` is true, the function stringifies the state as JSON before storing. + * + * @param {string} key - The key of the item to set + * @param {any} [state] - The value of the item to set + * @param {boolean} [isObject=false] - Whether to stringify the state as JSON + */ export const setSessionStorageItem = (key: string, state?: any, isObject: boolean = false) => { if (state) { sessionStorage.setItem(key, isObject ? JSON.stringify(state) : state); } }; +/** + * Removes an item from session storage by key. + * + * @param {string} key - The key of the item to remove + */ export const removeSessionStorageItem = (key: string) => { sessionStorage.removeItem(key); }; -// 获取之后就删除,多用于上一个页面传递数据到下一个页面 -export const getSessionItemThenRemove = (key: string, isObject: boolean = false) => { +/** + * Retrieves an item from session storage by key and then removes it. + * + * This function is useful for passing data from one page to another. + * + * @param {string} key - The key of the item to retrieve + * @param {boolean} [isObject=false] - Whether to parse the stored value as JSON + * @return {any} The retrieved item, or undefined if not found or parsing fails + */ +export const getSessionItemThenRemove = (key: string, isObject: boolean = false): any => { const res = getSessionStorageItem(key, isObject); sessionStorage.removeItem(key); return res;