| @@ -0,0 +1,44 @@ | |||
| .kf-basic-info { | |||
| display: flex; | |||
| flex-direction: row; | |||
| flex-wrap: wrap; | |||
| gap: 20px 40px; | |||
| align-items: flex-start; | |||
| width: 80%; | |||
| } | |||
| .kf-basic-info-item { | |||
| display: flex; | |||
| align-items: flex-start; | |||
| width: calc(50% - 20px); | |||
| font-size: 16px; | |||
| line-height: 1.6; | |||
| &__label { | |||
| position: relative; | |||
| color: @text-color-secondary; | |||
| text-align: justify; | |||
| text-align-last: justify; | |||
| &::after { | |||
| position: absolute; | |||
| content: ':'; | |||
| } | |||
| } | |||
| &__value { | |||
| flex: 1; | |||
| margin-left: 16px; | |||
| white-space: pre-line; | |||
| word-break: break-all; | |||
| } | |||
| &__text { | |||
| color: @text-color; | |||
| } | |||
| &__link:hover { | |||
| text-decoration: underline @underline-color; | |||
| text-underline-offset: 3px; | |||
| } | |||
| } | |||
| @@ -0,0 +1,73 @@ | |||
| import { isEmptyString } from '@/utils'; | |||
| import { Link } from '@umijs/max'; | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| export type BasicInfoData = { | |||
| label: string; | |||
| value?: any; | |||
| link?: string; | |||
| externalLink?: string; | |||
| format?: (_value?: any) => string | undefined; | |||
| }; | |||
| type BasicInfoProps = { | |||
| datas: BasicInfoData[]; | |||
| className?: string; | |||
| style?: React.CSSProperties; | |||
| labelWidth?: number; | |||
| }; | |||
| function BasicInfo({ datas, className, style, labelWidth = 100 }: BasicInfoProps) { | |||
| return ( | |||
| <div className={classNames('kf-basic-info', className)} style={style}> | |||
| {datas.map((item) => ( | |||
| <BasicInfoItem key={item.label} data={item} labelWidth={labelWidth} /> | |||
| ))} | |||
| </div> | |||
| ); | |||
| } | |||
| type BasicInfoItemProps = { | |||
| data: BasicInfoData; | |||
| labelWidth?: number; | |||
| }; | |||
| function BasicInfoItem({ data, labelWidth = 100 }: BasicInfoItemProps) { | |||
| const { label, value, externalLink, link, format } = data; | |||
| const showValue = format ? format(value) : value; | |||
| let valueComponent = undefined; | |||
| if (externalLink && showValue) { | |||
| valueComponent = ( | |||
| <a | |||
| className="kf-basic-info-item__value kf-basic-info-item__link" | |||
| href={externalLink} | |||
| target="_blank" | |||
| rel="noopener noreferrer" | |||
| > | |||
| {showValue} | |||
| </a> | |||
| ); | |||
| } else if (link && showValue) { | |||
| valueComponent = ( | |||
| <Link to={link} className="kf-basic-info-item__value kf-basic-info-item__link"> | |||
| {showValue} | |||
| </Link> | |||
| ); | |||
| } else { | |||
| valueComponent = ( | |||
| <div className="kf-basic-info-item__value kf-basic-info-item__text"> | |||
| {isEmptyString(showValue) ? '--' : showValue} | |||
| </div> | |||
| ); | |||
| } | |||
| return ( | |||
| <div className="kf-basic-info-item" key={label}> | |||
| <div className="kf-basic-info-item__label" style={{ width: labelWidth }}> | |||
| {label} | |||
| </div> | |||
| {valueComponent} | |||
| </div> | |||
| ); | |||
| } | |||
| export default BasicInfo; | |||
| @@ -61,7 +61,7 @@ function IframePage({ type, className, style }: IframePageProps) { | |||
| return ( | |||
| <div className={classNames('kf-iframe-page', className)} style={style}> | |||
| {loading && <KFSpin />} | |||
| {loading && <KFSpin size="large" />} | |||
| <FullScreenFrame url={iframeUrl} onload={hideLoading} onerror={hideLoading} /> | |||
| </div> | |||
| ); | |||
| @@ -0,0 +1,39 @@ | |||
| .kf-empty { | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| justify-content: center; | |||
| width: 100%; | |||
| &__image { | |||
| width: 475px; | |||
| } | |||
| &__title { | |||
| margin-top: 15px; | |||
| color: @text-color; | |||
| font-weight: 500; | |||
| font-size: 30px; | |||
| 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; | |||
| margin-bottom: 30px; | |||
| &__back-btn { | |||
| height: 32px; | |||
| } | |||
| } | |||
| } | |||
| @@ -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; | |||
| buttonTitle?: string; | |||
| onRefresh?: () => 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, | |||
| buttonTitle = '刷新', | |||
| onRefresh, | |||
| }: EmptyProps) { | |||
| const image = getEmptyImage(type); | |||
| return ( | |||
| <div className={classNames('kf-empty', className)} style={style}> | |||
| <img className="kf-empty__image" src={image} /> | |||
| <div className="kf-empty__title">{title}</div> | |||
| <div className="kf-empty__content">{content}</div> | |||
| {hasFooter && ( | |||
| <div className="kf-empty__footer"> | |||
| {footer ? ( | |||
| footer() | |||
| ) : ( | |||
| <Button className="kf-empty__footer__back-btn" type="primary" onClick={onRefresh}> | |||
| {buttonTitle} | |||
| </Button> | |||
| )} | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| export default KFEmpty; | |||
| @@ -4,7 +4,7 @@ | |||
| right: 0; | |||
| bottom: 0; | |||
| left: 0; | |||
| z-index: 1000; | |||
| z-index: 1001; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| @@ -14,7 +14,7 @@ const filterResourceStandard: SelectProps<string, ComputingResource>['filterOpti | |||
| }; | |||
| // id 从 number 转换为 string | |||
| const convertId = (item: any) => ({ ...item, id: String(item.id) }); | |||
| const convertId = (item: any) => ({ ...item, id: `${item.id}-${item.identifier}` }); | |||
| export type SelectPropsConfig = { | |||
| getOptions: () => Promise<any>; // 获取下拉数据 | |||
| @@ -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; | |||
| @@ -116,6 +116,10 @@ | |||
| } | |||
| } | |||
| .ant-input.ant-input-disabled { | |||
| height: 46px; | |||
| } | |||
| // 选择框高度为46px | |||
| .ant-select-single { | |||
| height: 46px; | |||
| @@ -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 = () => ( | |||
| <Result | |||
| status="404" | |||
| title="404" | |||
| subTitle="Sorry, the page you visited does not exist." | |||
| extra={ | |||
| <Button type="primary" onClick={() => history.push('/')}> | |||
| Back Home | |||
| </Button> | |||
| } | |||
| /> | |||
| ); | |||
| const NoFoundPage = () => { | |||
| const navigate = useNavigate(); | |||
| return ( | |||
| <KFEmpty | |||
| style={{ height: '100vh' }} | |||
| type={EmptyType.NotFound} | |||
| title="404" | |||
| content={'很抱歉,您访问的页面地址有误,\n或者该页面不存在。'} | |||
| hasFooter={true} | |||
| buttonTitle="返回首页" | |||
| onRefresh={() => navigate('/')} | |||
| ></KFEmpty> | |||
| ); | |||
| }; | |||
| export default NoFoundPage; | |||
| @@ -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<CodeConfigData[]>([]); | |||
| const [dataList, setDataList] = useState<CodeConfigData[] | undefined>(undefined); | |||
| const [total, setTotal] = useState(0); | |||
| const [pagination, setPagination] = useState<PaginationProps>({ | |||
| 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 ( | |||
| <div className={styles['code-config-list']}> | |||
| <div className={styles['code-config-list__header']}> | |||
| <span>数据总数:{total}个</span> | |||
| <span>数据总数:{total} 个</span> | |||
| <div> | |||
| <Input.Search | |||
| placeholder="按代码仓库名称筛选" | |||
| @@ -139,10 +143,10 @@ function CodeConfigList() { | |||
| </Button> | |||
| </div> | |||
| </div> | |||
| {dataList?.length !== 0 ? ( | |||
| {dataList && dataList.length !== 0 && ( | |||
| <> | |||
| <div className={styles['code-config-list__content']}> | |||
| {dataList?.map((item) => ( | |||
| {dataList.map((item) => ( | |||
| <CodeConfigItem | |||
| item={item} | |||
| key={item.id} | |||
| @@ -161,10 +165,16 @@ function CodeConfigList() { | |||
| {...pagination} | |||
| /> | |||
| </> | |||
| ) : ( | |||
| <div className={styles['code-config-list__empty']}> | |||
| <Empty></Empty> | |||
| </div> | |||
| )} | |||
| {dataList && dataList.length === 0 && ( | |||
| <KFEmpty | |||
| className={styles['code-config-list__empty']} | |||
| type={EmptyType.NoData} | |||
| title="暂无数据" | |||
| content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} | |||
| hasFooter={true} | |||
| onRefresh={getDataList} | |||
| /> | |||
| )} | |||
| </div> | |||
| ); | |||
| @@ -1,6 +1,7 @@ | |||
| .upload-tip { | |||
| margin-top: 5px; | |||
| color: @error-color; | |||
| color: @text-color-secondary; | |||
| font-size: 14px; | |||
| } | |||
| .upload-button { | |||
| @@ -1,9 +1,8 @@ | |||
| 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 { CategoryData, ResourceType, resourceConfig } from '@/pages/Dataset/config'; | |||
| import { addDataset } from '@/services/dataset/index.js'; | |||
| import { to } from '@/utils/promise'; | |||
| import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui'; | |||
| import { | |||
| @@ -19,8 +18,7 @@ import { | |||
| type UploadProps, | |||
| } from 'antd'; | |||
| import { omit } from 'lodash'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { CategoryData } from '../../config'; | |||
| import { useState } from 'react'; | |||
| import styles from './index.less'; | |||
| interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> { | |||
| @@ -31,15 +29,15 @@ interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> { | |||
| function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalProps) { | |||
| const [uuid] = useState(Date.now()); | |||
| const [clusterOptions, setClusterOptions] = useState<DictValueEnumObj[]>([]); | |||
| // const [clusterOptions, setClusterOptions] = useState<DictValueEnumObj[]>([]); | |||
| useEffect(() => { | |||
| getClusterOptions(); | |||
| }, []); | |||
| // useEffect(() => { | |||
| // getClusterOptions(); | |||
| // }, []); | |||
| // 上传组件参数 | |||
| const uploadProps: UploadProps = { | |||
| action: '/api/mmp/dataset/upload', | |||
| action: resourceConfig[ResourceType.Dataset].uploadAction, | |||
| 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(addDataset(params)); | |||
| if (res) { | |||
| message.success('创建成功'); | |||
| onOk?.(); | |||
| @@ -94,7 +92,13 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| }} | |||
| destroyOnClose | |||
| > | |||
| <Form name="form" layout="vertical" onFinish={onFinish} autoComplete="off"> | |||
| <Form | |||
| name="form" | |||
| layout="vertical" | |||
| onFinish={onFinish} | |||
| initialValues={{ is_public: false }} | |||
| autoComplete="off" | |||
| > | |||
| <Form.Item | |||
| label="数据集名称" | |||
| name="name" | |||
| @@ -106,7 +110,7 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入数据名称" showCount allowClear maxLength={64} /> | |||
| <Input placeholder="请输入数据名称" showCount allowClear maxLength={50} /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="数据集版本" | |||
| @@ -116,6 +120,14 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| required: true, | |||
| message: '请输入数据集版本', | |||
| }, | |||
| { | |||
| validator: (_rule, value) => { | |||
| if (value === 'master') { | |||
| return Promise.reject(`版本不能为 master`); | |||
| } | |||
| return Promise.resolve(); | |||
| }, | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入数据集版本" showCount allowClear maxLength={64} /> | |||
| @@ -125,7 +137,7 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| allowClear | |||
| placeholder="请选择数据集分类" | |||
| options={typeList} | |||
| fieldNames={{ label: 'name', value: 'id' }} | |||
| fieldNames={{ label: 'name', value: 'name' }} | |||
| optionFilterProp="name" | |||
| showSearch | |||
| /> | |||
| @@ -135,14 +147,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 | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item label="集群版本" name="available_cluster"> | |||
| {/* <Form.Item label="集群版本" name="available_cluster"> | |||
| <Select allowClear placeholder="请选择集群版本" options={clusterOptions} /> | |||
| </Form.Item> | |||
| </Form.Item> */} | |||
| <Form.Item | |||
| label="数据集简介" | |||
| name="description" | |||
| @@ -156,15 +168,19 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| <Input.TextArea | |||
| placeholder="请输入数据集简介" | |||
| showCount | |||
| maxLength={256} | |||
| maxLength={200} | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item label="选择流水线" name="range"> | |||
| <Form.Item | |||
| label="可见性" | |||
| name="is_public" | |||
| rules={[{ required: true, message: '请选择可见性' }]} | |||
| > | |||
| <Radio.Group> | |||
| <Radio value="0">仅自己可见</Radio> | |||
| <Radio value="1">工作空间可见</Radio> | |||
| <Radio value={false}>私有</Radio> | |||
| <Radio value={true}>公开</Radio> | |||
| </Radio.Group> | |||
| </Form.Item> | |||
| <Form.Item | |||
| @@ -187,7 +203,7 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| > | |||
| 上传文件 | |||
| </Button> | |||
| <div className={styles['upload-tip']}>只允许上传.zip,.tgz格式文件</div> | |||
| <div className={styles['upload-tip']}>只允许上传 .zip 和 .tgz 格式文件</div> | |||
| </Upload> | |||
| </Form.Item> | |||
| </Form> | |||
| @@ -1,7 +1,7 @@ | |||
| import { getAccessToken } from '@/access'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { CategoryData } from '@/pages/Dataset/config'; | |||
| import { CategoryData, ResourceType, resourceConfig } from '@/pages/Dataset/config'; | |||
| import { addModel } from '@/services/dataset/index.js'; | |||
| import { to } from '@/utils/promise'; | |||
| import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui'; | |||
| @@ -9,6 +9,7 @@ import { | |||
| Button, | |||
| Form, | |||
| Input, | |||
| Radio, | |||
| Select, | |||
| Upload, | |||
| UploadFile, | |||
| @@ -31,7 +32,7 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| // 上传组件参数 | |||
| const uploadProps: UploadProps = { | |||
| action: '/api/mmp/models/upload', | |||
| action: resourceConfig[ResourceType.Model].uploadAction, | |||
| headers: { | |||
| Authorization: getAccessToken() || '', | |||
| }, | |||
| @@ -53,7 +54,7 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| if (validateUploadFiles(fileList)) { | |||
| const params = { | |||
| ...omit(formData, ['fileList']), | |||
| models_version_vos: fileList.map((item) => { | |||
| model_version_vos: fileList.map((item) => { | |||
| const data = item.response?.data?.[0] ?? {}; | |||
| return { | |||
| file_name: data.fileName, | |||
| @@ -77,7 +78,13 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| form: 'form', | |||
| }} | |||
| > | |||
| <Form name="form" layout="vertical" onFinish={onFinish} autoComplete="off"> | |||
| <Form | |||
| name="form" | |||
| layout="vertical" | |||
| onFinish={onFinish} | |||
| autoComplete="off" | |||
| initialValues={{ is_public: false }} | |||
| > | |||
| <Form.Item | |||
| label="模型名称" | |||
| name="name" | |||
| @@ -88,9 +95,8 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入模型名称" showCount allowClear maxLength={64} /> | |||
| <Input placeholder="请输入模型名称" showCount allowClear maxLength={50} /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="模型版本" | |||
| name="version" | |||
| @@ -99,40 +105,24 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| required: true, | |||
| message: '请输入模型版本', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入模型版本" allowClear maxLength={64} /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="模型简介" | |||
| name="description" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入模型简介', | |||
| validator: (_rule, value) => { | |||
| if (value === 'master') { | |||
| return Promise.reject(`版本不能为 master`); | |||
| } | |||
| return Promise.resolve(); | |||
| }, | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| placeholder="请输入模型简介" | |||
| showCount | |||
| maxLength={256} | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| allowClear | |||
| /> | |||
| <Input placeholder="请输入模型版本" showCount allowClear maxLength={64} /> | |||
| </Form.Item> | |||
| {/* <Form.Item label="可见范围" name="available_range"> | |||
| <Radio.Group> | |||
| <Radio value="0">仅自己可见</Radio> | |||
| <Radio value="1">工作空间可见</Radio> | |||
| </Radio.Group> | |||
| </Form.Item> */} | |||
| <Form.Item label="模型框架" name="model_type"> | |||
| <Select | |||
| allowClear | |||
| placeholder="请选择模型类型" | |||
| options={typeList} | |||
| fieldNames={{ label: 'name', value: 'id' }} | |||
| fieldNames={{ label: 'name', value: 'name' }} | |||
| optionFilterProp="name" | |||
| showSearch | |||
| /> | |||
| @@ -142,11 +132,39 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| allowClear | |||
| placeholder="请选择模型标签" | |||
| options={tagList} | |||
| fieldNames={{ label: 'name', value: 'id' }} | |||
| fieldNames={{ label: 'name', value: 'name' }} | |||
| optionFilterProp="name" | |||
| showSearch | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="模型简介" | |||
| name="description" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入模型简介', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| placeholder="请输入模型简介" | |||
| maxLength={200} | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="可见性" | |||
| name="is_public" | |||
| rules={[{ required: true, message: '请选择可见性' }]} | |||
| > | |||
| <Radio.Group> | |||
| <Radio value={false}>私有</Radio> | |||
| <Radio value={true}>公开</Radio> | |||
| </Radio.Group> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="模型文件" | |||
| name="fileList" | |||
| @@ -21,14 +21,16 @@ import styles from '../AddDatasetModal/index.less'; | |||
| interface AddVersionModalProps extends Omit<ModalProps, 'onOk'> { | |||
| resourceType: ResourceType; | |||
| resourceId: number; | |||
| initialName: string; | |||
| identifier: string; | |||
| resoureName: string; | |||
| onOk: () => void; | |||
| } | |||
| function AddVersionModal({ | |||
| resourceType, | |||
| resourceId, | |||
| initialName, | |||
| resoureName, | |||
| identifier, | |||
| onOk, | |||
| ...rest | |||
| }: AddVersionModalProps) { | |||
| @@ -58,17 +60,20 @@ function AddVersionModal({ | |||
| const onFinish = (formData: any) => { | |||
| const fileList: UploadFile[] = formData['fileList'] ?? []; | |||
| if (validateUploadFiles(fileList)) { | |||
| const otherParams = omit(formData, ['fileList']); | |||
| const params = fileList.map((item) => { | |||
| const version_vos = fileList.map((item) => { | |||
| const data = item.response?.data?.[0] ?? {}; | |||
| return { | |||
| ...otherParams, | |||
| [config.idParamKey]: resourceId, | |||
| file_name: data.fileName, | |||
| file_size: data.fileSize, | |||
| url: data.url, | |||
| }; | |||
| }); | |||
| const params = { | |||
| id: resourceId, | |||
| identifier, | |||
| [config.filePropKey]: version_vos, | |||
| ...omit(formData, 'fileList'), | |||
| }; | |||
| createDatasetVersion(params); | |||
| } | |||
| }; | |||
| @@ -90,7 +95,7 @@ function AddVersionModal({ | |||
| name="form" | |||
| layout="vertical" | |||
| initialValues={{ | |||
| name: initialName, | |||
| name: resoureName, | |||
| }} | |||
| onFinish={onFinish} | |||
| autoComplete="off" | |||
| @@ -115,13 +120,21 @@ function AddVersionModal({ | |||
| required: true, | |||
| message: `请输入${name}版本`, | |||
| }, | |||
| { | |||
| validator: (_rule, value) => { | |||
| if (value === 'master') { | |||
| return Promise.reject(`版本不能为 master`); | |||
| } | |||
| return Promise.resolve(); | |||
| }, | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder={`请输入${name}版本`} maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="版本描述" | |||
| name="description" | |||
| name="version_desc" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| @@ -158,7 +171,7 @@ function AddVersionModal({ | |||
| 上传文件 | |||
| </Button> | |||
| {resourceType === ResourceType.Dataset && ( | |||
| <div className={styles['upload-tip']}>只允许上传.zip格式文件</div> | |||
| <div className={styles['upload-tip']}>只允许上传 .zip 格式文件</div> | |||
| )} | |||
| </Upload> | |||
| </Form.Item> | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,235 @@ | |||
| /* | |||
| * @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<ResourceData>({} 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<ResourceVersionData[]>([]); | |||
| const [version, setVersion] = useState<string | undefined>(undefined); | |||
| const [activeTab, setActiveTab] = useState<string>(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 = { | |||
| ...pick(info, ['id', 'owner', 'identifier', 'relative_paths']), | |||
| version, | |||
| }; | |||
| const [res] = await to(request(params)); | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| getVersionList(pick(info, ['owner', 'identifier'])); | |||
| } | |||
| }; | |||
| // 处理删除 | |||
| const hanldeDelete = () => { | |||
| modalConfirm({ | |||
| title: '删除后,该版本将不可恢复', | |||
| content: '是否确认删除?', | |||
| okText: '确认', | |||
| cancelText: '取消', | |||
| onOk: () => { | |||
| deleteVersion(); | |||
| }, | |||
| }); | |||
| }; | |||
| const items = [ | |||
| { | |||
| key: ResourceInfoTabKeys.Introduction, | |||
| label: `${typeName}简介`, | |||
| icon: <KFIcon type="icon-moxingjianjie" />, | |||
| children: <ResourceIntro resourceType={resourceType} info={info}></ResourceIntro>, | |||
| }, | |||
| { | |||
| key: ResourceInfoTabKeys.Version, | |||
| label: `${typeName}文件`, | |||
| icon: <KFIcon type="icon-moxingwenjian" />, | |||
| children: <ResourceVersion resourceType={resourceType} info={info}></ResourceVersion>, | |||
| }, | |||
| ]; | |||
| if (resourceType === ResourceType.Model) { | |||
| items.push({ | |||
| key: ResourceInfoTabKeys.Evolution, | |||
| label: `模型演化`, | |||
| icon: <KFIcon type="icon-moxingyanhua1" />, | |||
| children: ( | |||
| <ModelEvolution | |||
| resourceId={resourceId} | |||
| version={version} | |||
| identifier={info.identifier} | |||
| isActive={activeTab === ResourceInfoTabKeys.Evolution} | |||
| onVersionChange={handleVersionChange} | |||
| ></ModelEvolution> | |||
| ), | |||
| }); | |||
| } | |||
| const typePropertyName = config.typeParamKey as keyof ResourceData; | |||
| const tagPropertyName = config.tagParamKey as keyof ResourceData; | |||
| return ( | |||
| <div className={styles['resource-info']}> | |||
| <div className={styles['resource-info__top']}> | |||
| <Flex align="center" gap={10} style={{ marginBottom: '20px' }}> | |||
| <div className={styles['resource-info__top__name']}>{info.name}</div> | |||
| {info[typePropertyName] && ( | |||
| <div className={styles['resource-info__top__tag']}> | |||
| {(info[typePropertyName] as string) || '--'} | |||
| </div> | |||
| )} | |||
| {info[tagPropertyName] && ( | |||
| <div className={styles['resource-info__top__tag']}> | |||
| {(info[tagPropertyName] as string) || '--'} | |||
| </div> | |||
| )} | |||
| </Flex> | |||
| <Flex align="center"> | |||
| <span style={{ marginRight: '10px' }}>版本号:</span> | |||
| <Select | |||
| placeholder="请选择版本号" | |||
| style={{ width: '160px', marginRight: '20px' }} | |||
| value={version} | |||
| onChange={handleVersionChange} | |||
| fieldNames={{ label: 'name', value: 'name' }} | |||
| options={versionList} | |||
| /> | |||
| <Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}> | |||
| 创建新版本 | |||
| </Button> | |||
| <Button | |||
| type="default" | |||
| style={{ marginLeft: 'auto', marginRight: 0 }} | |||
| onClick={hanldeDelete} | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| disabled={!version} | |||
| danger | |||
| > | |||
| 删除版本 | |||
| </Button> | |||
| </Flex> | |||
| </div> | |||
| <div className={styles['resource-info__bottom']}> | |||
| <Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs> | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default ResourceInfo; | |||
| @@ -1,65 +1,10 @@ | |||
| .resource-intro { | |||
| height: 100%; | |||
| &__top { | |||
| width: 100%; | |||
| margin-top: 24px; | |||
| &__basic { | |||
| width: 100%; | |||
| height: 110px; | |||
| margin-bottom: 10px; | |||
| padding: 20px 30px 0; | |||
| background-image: url(@/assets/img/dataset-intro-top.png); | |||
| background-repeat: no-repeat; | |||
| background-position: top center; | |||
| background-size: 100% 100%; | |||
| &__name { | |||
| margin-bottom: 12px; | |||
| color: @text-color; | |||
| font-size: 20px; | |||
| } | |||
| &__tag { | |||
| margin-right: 10px; | |||
| padding: 4px 10px; | |||
| color: @primary-color; | |||
| font-size: 14px; | |||
| background: rgba(22, 100, 255, 0.1); | |||
| border-radius: 4px; | |||
| } | |||
| } | |||
| &__bottom { | |||
| height: calc(100% - 120px); | |||
| 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; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| &__title { | |||
| margin: 30px 0 10px; | |||
| color: @text-color; | |||
| font-weight: 500; | |||
| font-size: @font-size; | |||
| } | |||
| &__intro { | |||
| color: @text-color-secondary; | |||
| font-size: 14px; | |||
| &__usage { | |||
| width: 100%; | |||
| } | |||
| } | |||
| @@ -1,156 +1,168 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import ModelEvolution from '@/pages/Model/components/ModelEvolution'; | |||
| import { to } from '@/utils/promise'; | |||
| import { useParams, useSearchParams } from '@umijs/max'; | |||
| import { Flex, Tabs } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { ResourceData, ResourceType, resourceConfig } from '../../config'; | |||
| import ResourceVersion from '../ResourceVersion'; | |||
| import BasicInfo, { BasicInfoData } from '@/components/BasicInfo'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { ResourceData, ResourceType } from '@/pages/Dataset/config'; | |||
| import styles from './index.less'; | |||
| // 这里值小写是因为值会写在 url 中 | |||
| export enum ResourceInfoTabKeys { | |||
| Introduction = 'introduction', // 简介 | |||
| Version = 'version', // 版本 | |||
| Evolution = 'evolution', // 演化 | |||
| } | |||
| type ResourceIntroProps = { | |||
| resourceType: ResourceType; | |||
| info: ResourceData; | |||
| }; | |||
| const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | |||
| const [info, setInfo] = useState<ResourceData>({} as ResourceData); | |||
| const locationParams = useParams(); | |||
| const [searchParams] = useSearchParams(); | |||
| const defaultTab = searchParams.get('tab') || ResourceInfoTabKeys.Introduction; | |||
| let versionParam = searchParams.get('version'); | |||
| const [versionList, setVersionList] = useState([]); | |||
| const [version, setVersion] = useState<string | undefined>(undefined); | |||
| const [activeTab, setActiveTab] = useState<string>(defaultTab); | |||
| const resourceId = Number(locationParams.id); | |||
| const config = resourceConfig[resourceType]; | |||
| const typeName = config.name; // 数据集/模型 | |||
| useEffect(() => { | |||
| getModelByDetail(); | |||
| getVersionList(); | |||
| }, [resourceId]); | |||
| // 获取详情 | |||
| const getModelByDetail = async () => { | |||
| const request = config.getInfo; | |||
| const [res] = await to(request(resourceId)); | |||
| if (res) { | |||
| setInfo(res.data); | |||
| } | |||
| }; | |||
| // 获取版本列表 | |||
| const getVersionList = async () => { | |||
| const request = config.getVersions; | |||
| const [res] = await to(request(resourceId)); | |||
| if (res && res.data && res.data.length > 0) { | |||
| setVersionList( | |||
| res.data.map((item: string) => { | |||
| return { | |||
| label: item, | |||
| value: item, | |||
| }; | |||
| }), | |||
| ); | |||
| if (versionParam && res.data.includes(versionParam)) { | |||
| setVersion(versionParam); | |||
| versionParam = null; | |||
| } else { | |||
| setVersion(res.data[0]); | |||
| } | |||
| } else { | |||
| setVersion(undefined); | |||
| } | |||
| }; | |||
| const formatArray = (arr?: string[]) => { | |||
| if (!arr || arr.length === 0) { | |||
| return '--'; | |||
| } | |||
| return arr.join('\n'); | |||
| }; | |||
| // 版本变化 | |||
| const handleVersionChange = (value: string) => { | |||
| setVersion(value); | |||
| }; | |||
| const formatMap = (map?: Record<string, string>) => { | |||
| if (!map || Object.keys(map).length === 0) { | |||
| return '--'; | |||
| } | |||
| return Object.entries(map) | |||
| .map(([key, value]) => `${key} = ${value}`) | |||
| .join('\n'); | |||
| }; | |||
| const items = [ | |||
| { | |||
| key: ResourceInfoTabKeys.Introduction, | |||
| label: `${typeName}简介`, | |||
| icon: <KFIcon type="icon-moxingjianjie" />, | |||
| children: ( | |||
| <> | |||
| <div className={styles['resource-intro__title']}>简介</div> | |||
| <div className={styles['resource-intro__intro']}>{info.description}</div> | |||
| </> | |||
| ), | |||
| }, | |||
| { | |||
| key: ResourceInfoTabKeys.Version, | |||
| label: `${typeName}文件/版本`, | |||
| icon: <KFIcon type="icon-moxingwenjian" />, | |||
| children: ( | |||
| <ResourceVersion | |||
| resourceType={resourceType} | |||
| resourceId={resourceId} | |||
| resourceName={info.name} | |||
| isPublic={info.available_range === 1} | |||
| versionList={versionList} | |||
| version={version} | |||
| isActive={activeTab === ResourceInfoTabKeys.Version} | |||
| getVersionList={getVersionList} | |||
| onVersionChange={handleVersionChange} | |||
| ></ResourceVersion> | |||
| ), | |||
| function ResourceIntro({ resourceType, info }: ResourceIntroProps) { | |||
| const datasetDatas: BasicInfoData[] = [ | |||
| { | |||
| label: '数据集名称', | |||
| value: info.name, | |||
| }, | |||
| { | |||
| label: '版本', | |||
| value: info.version, | |||
| }, | |||
| { | |||
| label: '创建人', | |||
| value: info.create_by, | |||
| }, | |||
| { | |||
| label: '更新时间', | |||
| value: info.update_time, | |||
| }, | |||
| { | |||
| label: '数据来源', | |||
| value: info.dataset_source, | |||
| }, | |||
| { | |||
| label: '处理代码', | |||
| value: info.processing_code, | |||
| }, | |||
| { | |||
| label: '数据集分类', | |||
| value: info.data_type, | |||
| }, | |||
| { | |||
| label: '研究方向', | |||
| value: info.data_tag, | |||
| }, | |||
| { | |||
| label: '数据集描述', | |||
| value: info.description, | |||
| }, | |||
| { | |||
| label: '版本描述', | |||
| value: info.version_desc, | |||
| }, | |||
| ]; | |||
| if (resourceType === ResourceType.Model) { | |||
| items.push({ | |||
| key: ResourceInfoTabKeys.Evolution, | |||
| label: `模型演化`, | |||
| icon: <KFIcon type="icon-moxingyanhua1" />, | |||
| children: ( | |||
| <ModelEvolution | |||
| resourceId={resourceId} | |||
| versionList={versionList} | |||
| version={version} | |||
| isActive={activeTab === ResourceInfoTabKeys.Evolution} | |||
| onVersionChange={handleVersionChange} | |||
| ></ModelEvolution> | |||
| ), | |||
| }); | |||
| } | |||
| const modelDatas: BasicInfoData[] = [ | |||
| { | |||
| label: '模型名称', | |||
| value: info.name, | |||
| }, | |||
| { | |||
| label: '版本', | |||
| value: info.version, | |||
| }, | |||
| { | |||
| label: '创建人', | |||
| value: info.create_by, | |||
| }, | |||
| { | |||
| label: '更新时间', | |||
| value: info.update_time, | |||
| }, | |||
| { | |||
| label: '训练镜像', | |||
| value: info.image, | |||
| }, | |||
| { | |||
| label: '训练代码', | |||
| value: info.code, | |||
| }, | |||
| { | |||
| label: '训练数据集', | |||
| value: info.train_datasets, | |||
| format: formatArray, | |||
| }, | |||
| { | |||
| label: '测试数据集', | |||
| value: info.test_datasets, | |||
| format: formatArray, | |||
| }, | |||
| { | |||
| label: '参数', | |||
| value: info.params, | |||
| format: formatMap, | |||
| }, | |||
| { | |||
| label: '指标', | |||
| value: info.metrics, | |||
| format: formatMap, | |||
| }, | |||
| { | |||
| label: '训练任务', | |||
| value: info.train_task, | |||
| }, | |||
| { | |||
| label: '模型来源', | |||
| value: info.model_source, | |||
| }, | |||
| { | |||
| label: '模型框架', | |||
| value: info.model_type, | |||
| }, | |||
| { | |||
| label: '模型能力', | |||
| value: info.model_tag, | |||
| }, | |||
| { | |||
| label: '模型描述', | |||
| value: info.description, | |||
| }, | |||
| { | |||
| label: '版本描述', | |||
| value: info.version_desc, | |||
| }, | |||
| ]; | |||
| const infoTypePropertyName = config.infoTypePropertyName as keyof ResourceData; | |||
| const infoTagPropertyName = config.infoTagPropertyName as keyof ResourceData; | |||
| const basicDatas: BasicInfoData[] = | |||
| resourceType === ResourceType.Dataset ? datasetDatas : modelDatas; | |||
| return ( | |||
| <div className={styles['resource-intro']}> | |||
| <div className={styles['resource-intro__top']}> | |||
| <div className={styles['resource-intro__top__name']}>{info.name}</div> | |||
| <Flex align="center"> | |||
| <div className={styles['resource-intro__top__tag']}> | |||
| {typeName} id:{info.id} | |||
| </div> | |||
| {info[infoTypePropertyName] && ( | |||
| <div className={styles['resource-intro__top__tag']}> | |||
| {info[infoTypePropertyName] || '--'} | |||
| </div> | |||
| )} | |||
| {info[infoTagPropertyName] && ( | |||
| <div className={styles['resource-intro__top__tag']}> | |||
| {info[infoTagPropertyName] || '--'} | |||
| </div> | |||
| )} | |||
| </Flex> | |||
| </div> | |||
| <div className={styles['resource-intro__bottom']}> | |||
| <Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs> | |||
| <SubAreaTitle | |||
| title="基本信息" | |||
| image={require('@/assets/img/mirror-basic.png')} | |||
| style={{ marginBottom: '26px' }} | |||
| ></SubAreaTitle> | |||
| <div className={styles['resource-intro__basic']}> | |||
| <BasicInfo datas={basicDatas} labelWidth={86}></BasicInfo> | |||
| </div> | |||
| <SubAreaTitle | |||
| title="实例用法" | |||
| image={require('@/assets/img/usage-icon.png')} | |||
| style={{ margin: '40px 0 24px' }} | |||
| ></SubAreaTitle> | |||
| <div | |||
| className={styles['resource-intro__usage']} | |||
| dangerouslySetInnerHTML={{ __html: info.usage ?? '暂无实例用法' }} | |||
| ></div> | |||
| </div> | |||
| ); | |||
| }; | |||
| } | |||
| export default ResourceIntro; | |||
| @@ -40,11 +40,14 @@ function ResourceItem({ item, isPublic, onClick, onRemove }: ResourceItemProps) | |||
| <Flex justify="space-between"> | |||
| <div className={styles['resource-item__time']}> | |||
| <img style={{ width: '17px', marginRight: '6px' }} src={creatByImg} alt="" /> | |||
| <span>{item.create_by}</span> | |||
| <span>{item.create_by ?? ''}</span> | |||
| </div> | |||
| <div className={styles['resource-item__time']}> | |||
| <img style={{ width: '12px', marginRight: '5px' }} src={clock} alt="" /> | |||
| <span>最近更新: {formatDate(item.update_time, 'YYYY-MM-DD')}</span> | |||
| <span> | |||
| {'最近更新: '} | |||
| {item.update_time ? formatDate(item.update_time, 'YYYY-MM-DD') : item.time_ago ?? ''} | |||
| </span> | |||
| </div> | |||
| </Flex> | |||
| </div> | |||
| @@ -36,4 +36,8 @@ | |||
| text-align: right; | |||
| } | |||
| } | |||
| &__empty { | |||
| flex: 1; | |||
| } | |||
| } | |||
| @@ -1,11 +1,14 @@ | |||
| import KFEmpty, { EmptyType } from '@/components/KFEmpty'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import AddModelModal from '@/pages/Dataset/components/AddModelModal'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import { resourceItemKey, setSessionStorageItem } from '@/utils/sessionStorage'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { useNavigate } from '@umijs/max'; | |||
| import { App, Button, Input, Pagination, PaginationProps } from 'antd'; | |||
| import { pick } from 'lodash'; | |||
| import { Ref, forwardRef, useEffect, useImperativeHandle, useState } from 'react'; | |||
| import { CategoryData, ResourceData, ResourceType, resourceConfig } from '../../config'; | |||
| import AddDatasetModal from '../AddDatasetModal'; | |||
| @@ -43,7 +46,7 @@ function ResourceList( | |||
| ref: Ref<ResourceListRef>, | |||
| ) { | |||
| const navigate = useNavigate(); | |||
| const [dataList, setDataList] = useState<ResourceData[]>([]); | |||
| const [dataList, setDataList] = useState<ResourceData[] | undefined>(undefined); | |||
| const [total, setTotal] = useState(0); | |||
| const [pagination, setPagination] = useState<PaginationProps>( | |||
| initialPagination ?? { | |||
| @@ -71,7 +74,8 @@ function ResourceList( | |||
| }); | |||
| setSearchText(''); | |||
| setInputText(''); | |||
| setDataList([]); | |||
| setDataList(undefined); | |||
| setTotal(0); | |||
| }, | |||
| }; | |||
| }, | |||
| @@ -80,12 +84,12 @@ function ResourceList( | |||
| // 获取数据请求 | |||
| const getDataList = async () => { | |||
| const params = { | |||
| const params: Record<string, any> = { | |||
| page: pagination.current! - 1, | |||
| size: pagination.pageSize, | |||
| is_public: isPublic, | |||
| [config.typeParamKey]: dataType, | |||
| [config.tagParamKey]: dataTag, | |||
| available_range: isPublic ? 1 : 0, | |||
| name: searchText !== '' ? searchText : undefined, | |||
| }; | |||
| const request = config.getList; | |||
| @@ -93,13 +97,16 @@ function ResourceList( | |||
| if (res && res.data && res.data.content) { | |||
| setDataList(res.data.content); | |||
| setTotal(res.data.totalElements); | |||
| } else { | |||
| setDataList([]); | |||
| setTotal(0); | |||
| } | |||
| }; | |||
| // 删除请求 | |||
| const deleteRecord = async (id: number) => { | |||
| const deleteRecord = async (params: { owner: string; identifier: string; repo_id?: number }) => { | |||
| const request = config.deleteRecord; | |||
| const [res] = await to(request(id)); | |||
| const [res] = await to(request(params)); | |||
| if (res) { | |||
| getDataList(); | |||
| message.success('删除成功'); | |||
| @@ -116,7 +123,7 @@ function ResourceList( | |||
| modalConfirm({ | |||
| title: config.deleteModalTitle, | |||
| onOk: () => { | |||
| deleteRecord(record.id); | |||
| deleteRecord(pick(record, ['owner', 'identifier', 'id'])); | |||
| }, | |||
| }); | |||
| }; | |||
| @@ -131,6 +138,7 @@ function ResourceList( | |||
| activeTag: dataTag, | |||
| }); | |||
| const prefix = config.prefix; | |||
| setSessionStorageItem(resourceItemKey, record, true); | |||
| navigate(`/dataset/${prefix}/info/${record.id}`); | |||
| }; | |||
| @@ -158,7 +166,7 @@ function ResourceList( | |||
| return ( | |||
| <div className={styles['resource-list']}> | |||
| <div className={styles['resource-list__header']}> | |||
| <span>数据总数:{total}个</span> | |||
| <span>数据总数:{total} 个</span> | |||
| <div> | |||
| <Input.Search | |||
| placeholder={`按${config.name}名称筛选`} | |||
| @@ -182,26 +190,40 @@ function ResourceList( | |||
| )} | |||
| </div> | |||
| </div> | |||
| <div className={styles['resource-list__content']}> | |||
| {dataList?.map((item) => ( | |||
| <ResourceItem | |||
| item={item} | |||
| key={item.id} | |||
| isPublic={isPublic} | |||
| onRemove={handleRemove} | |||
| onClick={handleClick} | |||
| ></ResourceItem> | |||
| ))} | |||
| </div> | |||
| <Pagination | |||
| total={total} | |||
| showSizeChanger | |||
| defaultPageSize={20} | |||
| pageSizeOptions={[20, 40, 60, 80, 100]} | |||
| showQuickJumper | |||
| onChange={handlePageChange} | |||
| {...pagination} | |||
| /> | |||
| {dataList && dataList.length > 0 && ( | |||
| <> | |||
| <div className={styles['resource-list__content']}> | |||
| {dataList?.map((item) => ( | |||
| <ResourceItem | |||
| item={item} | |||
| key={item.id} | |||
| isPublic={isPublic} | |||
| onRemove={handleRemove} | |||
| onClick={handleClick} | |||
| ></ResourceItem> | |||
| ))} | |||
| </div> | |||
| <Pagination | |||
| total={total} | |||
| showSizeChanger | |||
| defaultPageSize={20} | |||
| pageSizeOptions={[20, 40, 60, 80, 100]} | |||
| showQuickJumper | |||
| onChange={handlePageChange} | |||
| {...pagination} | |||
| /> | |||
| </> | |||
| )} | |||
| {dataList && dataList.length === 0 && ( | |||
| <KFEmpty | |||
| className={styles['resource-list__empty']} | |||
| type={EmptyType.NoData} | |||
| title="暂无数据" | |||
| content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} | |||
| hasFooter={true} | |||
| onRefresh={getDataList} | |||
| /> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -1,125 +1,36 @@ | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| import DateTableCell from '@/components/DateTableCell'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { useEffectWhen } from '@/hooks'; | |||
| import AddVersionModal from '@/pages/Dataset/components/AddVersionModal'; | |||
| import { | |||
| ResourceData, | |||
| ResourceFileData, | |||
| ResourceType, | |||
| ResourceVersionData, | |||
| resourceConfig, | |||
| } from '@/pages/Dataset/config'; | |||
| import { downLoadZip } from '@/utils/downloadfile'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { App, Button, Flex, Select, Table } from 'antd'; | |||
| import { useState } from 'react'; | |||
| import { Button, Flex, Table } from 'antd'; | |||
| import styles from './index.less'; | |||
| type ResourceVersionProps = { | |||
| resourceType: ResourceType; | |||
| resourceId: number; | |||
| resourceName: string; | |||
| isPublic: boolean; | |||
| versionList: ResourceVersionData[]; | |||
| version?: string; | |||
| isActive: boolean; | |||
| getVersionList: () => void; | |||
| onVersionChange: (version: string) => void; | |||
| info: ResourceData; | |||
| }; | |||
| function ResourceVersion({ | |||
| resourceType, | |||
| resourceId, | |||
| resourceName, | |||
| isPublic, | |||
| versionList, | |||
| version, | |||
| isActive, | |||
| getVersionList, | |||
| onVersionChange, | |||
| }: ResourceVersionProps) { | |||
| const [fileList, setFileList] = useState<ResourceFileData[]>([]); | |||
| const { message } = App.useApp(); | |||
| function ResourceVersion({ resourceType, info }: ResourceVersionProps) { | |||
| const config = resourceConfig[resourceType]; | |||
| // 获取版本文件列表 | |||
| useEffectWhen( | |||
| () => { | |||
| if (version) { | |||
| getFileList(version); | |||
| } else { | |||
| setFileList([]); | |||
| } | |||
| }, | |||
| [resourceId, version], | |||
| isActive, | |||
| ); | |||
| // 获取版本下的文件列表 | |||
| const getFileList = async (version: string) => { | |||
| const params = { | |||
| version, | |||
| [config.fileReqParamKey]: resourceId, | |||
| }; | |||
| const request = config.getFiles; | |||
| const [res] = await to(request(params)); | |||
| if (res) { | |||
| setFileList(res?.data?.content ?? []); | |||
| } | |||
| }; | |||
| // 删除版本 | |||
| const deleteVersion = async () => { | |||
| const request = config.deleteVersion; | |||
| const params = { | |||
| [config.idParamKey]: resourceId, | |||
| version, | |||
| }; | |||
| const [res] = await to(request(params)); | |||
| if (res) { | |||
| getVersionList(); | |||
| message.success('删除成功'); | |||
| } | |||
| }; | |||
| // 新建版本 | |||
| const showModal = () => { | |||
| const { close } = openAntdModal(AddVersionModal, { | |||
| resourceType: resourceType, | |||
| resourceId: resourceId, | |||
| initialName: resourceName, | |||
| onOk: () => { | |||
| getVersionList(); | |||
| close(); | |||
| }, | |||
| }); | |||
| }; | |||
| // 处理删除 | |||
| const hanldeDelete = () => { | |||
| modalConfirm({ | |||
| title: '删除后,该版本将不可恢复', | |||
| content: '是否确认删除?', | |||
| okText: '确认', | |||
| cancelText: '取消', | |||
| onOk: () => { | |||
| deleteVersion(); | |||
| }, | |||
| }); | |||
| }; | |||
| const filePropKey = config.filePropKey as keyof ResourceData; | |||
| const fileList = (info[filePropKey] ?? []) as ResourceFileData[]; | |||
| fileList.forEach((item) => (item.update_time = info.update_time)); | |||
| // 全部导出 | |||
| const handleExport = async () => { | |||
| const url = config.downloadAllAction; | |||
| downLoadZip(url, { models_id: resourceId, version }); | |||
| downLoadZip(url, { name: info.name, id: info.id, version: info.version }); | |||
| }; | |||
| // 单个导出 | |||
| const downloadAlone = (record: ResourceFileData) => { | |||
| const downloadAlone = async (record: ResourceFileData) => { | |||
| const url = config.downloadSingleAction; | |||
| downLoadZip(`${url}/${record.id}`); | |||
| downLoadZip(url, { url: record.url }); | |||
| }; | |||
| const columns = [ | |||
| @@ -142,12 +53,6 @@ function ResourceVersion({ | |||
| </a> | |||
| ), | |||
| }, | |||
| { | |||
| title: '版本号', | |||
| dataIndex: 'version', | |||
| key: 'version', | |||
| render: CommonTableCell(), | |||
| }, | |||
| { | |||
| title: '文件大小', | |||
| dataIndex: 'file_size', | |||
| @@ -163,7 +68,7 @@ function ResourceVersion({ | |||
| { | |||
| title: '操作', | |||
| dataIndex: 'option', | |||
| width: '100px', | |||
| width: 160, | |||
| key: 'option', | |||
| render: (_: any, record: ResourceFileData) => [ | |||
| <Button | |||
| @@ -183,32 +88,9 @@ function ResourceVersion({ | |||
| <div className={styles['resource-version']}> | |||
| <Flex justify="space-between" align="center" style={{ margin: '30px 0' }}> | |||
| <Flex align="center"> | |||
| <span style={{ marginRight: '10px' }}>版本号:</span> | |||
| <Select | |||
| placeholder="请选择版本号" | |||
| style={{ width: '160px', marginRight: '20px' }} | |||
| value={version} | |||
| onChange={onVersionChange} | |||
| options={versionList} | |||
| /> | |||
| <Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}> | |||
| 创建新版本 | |||
| </Button> | |||
| </Flex> | |||
| <Flex align="center"> | |||
| {!isPublic && ( | |||
| <Button | |||
| type="default" | |||
| style={{ marginRight: '20px' }} | |||
| onClick={hanldeDelete} | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| > | |||
| 删除 | |||
| </Button> | |||
| )} | |||
| <Button | |||
| type="default" | |||
| disabled={!version} | |||
| disabled={fileList.length === 0} | |||
| onClick={handleExport} | |||
| icon={<KFIcon type="icon-xiazai" />} | |||
| > | |||
| @@ -216,12 +98,7 @@ function ResourceVersion({ | |||
| </Button> | |||
| </Flex> | |||
| </Flex> | |||
| <div style={{ marginBottom: '30px', fontSize: '15px' }}> | |||
| {fileList.length > 0 && fileList[0].description | |||
| ? '版本描述:' + fileList[0].description | |||
| : null} | |||
| </div> | |||
| <Table columns={columns} dataSource={fileList} pagination={false} rowKey="id" /> | |||
| <Table columns={columns} dataSource={fileList} pagination={false} rowKey="url" /> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -1,20 +1,18 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { | |||
| addDatasetVersionDetail, | |||
| addModelsVersionDetail, | |||
| addDatasetVersion, | |||
| addModelVersion, | |||
| deleteDataset, | |||
| deleteDatasetVersion, | |||
| deleteModel, | |||
| deleteModelVersion, | |||
| getDatasetById, | |||
| getDatasetInfo, | |||
| getDatasetList, | |||
| getDatasetVersionIdList, | |||
| getDatasetVersionsById, | |||
| getModelById, | |||
| getDatasetVersionList, | |||
| getModelInfo, | |||
| getModelList, | |||
| getModelVersionIdList, | |||
| getModelVersionsById, | |||
| getModelVersionList, | |||
| } from '@/services/dataset/index.js'; | |||
| import type { TabsProps } from 'antd'; | |||
| @@ -26,7 +24,6 @@ export enum ResourceType { | |||
| type ResourceTypeInfo = { | |||
| getList: (params: any) => Promise<any>; // 获取资源列表 | |||
| getVersions: (params: any) => Promise<any>; // 获取版本列表 | |||
| getFiles: (params: any) => Promise<any>; // 获取版本下的文件列表 | |||
| deleteRecord: (params: any) => Promise<any>; // 删除 | |||
| addVersion: (params: any) => Promise<any>; // 新增版本 | |||
| deleteVersion: (params: any) => Promise<any>; // 删除版本 | |||
| @@ -34,7 +31,7 @@ type ResourceTypeInfo = { | |||
| name: string; // 名称 | |||
| typeParamKey: string; // 类型参数名称,获取资源列表接口使用 | |||
| tagParamKey: string; // 标签参数名称,获取资源列表接口使用 | |||
| fileReqParamKey: 'models_id' | 'dataset_id'; // 文件请求参数名称,获取文件列表接口使用 | |||
| filePropKey: string; | |||
| tabItems: TabsProps['items']; // tab 列表 | |||
| typeTitle: string; // 类型标题 | |||
| tagTitle: string; // 标签标题 | |||
| @@ -43,28 +40,24 @@ type ResourceTypeInfo = { | |||
| prefix: string; // 图片资源、详情 url 的前缀 | |||
| deleteModalTitle: string; // 删除弹框的title | |||
| addBtnTitle: string; // 新增按钮的title | |||
| idParamKey: 'models_id' | 'dataset_id'; // 新建版本、删除版本接口,版本 id 的参数名称 | |||
| uploadAction: string; // 上传接口 url | |||
| uploadAccept?: string; // 上传文件类型 | |||
| downloadAllAction: string; // 批量下载接口 url | |||
| downloadSingleAction: string; // 单个下载接口 url | |||
| infoTypePropertyName: string; // 详情数据中,类型属性名称 | |||
| infoTagPropertyName: string; // 详情数据中,标签属性名称 | |||
| }; | |||
| export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = { | |||
| [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', | |||
| fileReqParamKey: 'dataset_id', | |||
| filePropKey: 'dataset_version_vos', | |||
| tabItems: [ | |||
| { | |||
| key: CommonTabKeys.Public, | |||
| @@ -84,26 +77,22 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = { | |||
| prefix: 'dataset', | |||
| 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', | |||
| }, | |||
| [ResourceType.Model]: { | |||
| getList: getModelList, | |||
| getVersions: getModelVersionsById, | |||
| getFiles: getModelVersionIdList, | |||
| getVersions: getModelVersionList, | |||
| deleteRecord: deleteModel, | |||
| addVersion: addModelsVersionDetail, | |||
| addVersion: addModelVersion, | |||
| deleteVersion: deleteModelVersion, | |||
| getInfo: getModelById, | |||
| getInfo: getModelInfo, | |||
| name: '模型', | |||
| typeParamKey: 'model_type', | |||
| tagParamKey: 'model_tag', | |||
| fileReqParamKey: 'models_id', | |||
| filePropKey: 'model_version_vos', | |||
| tabItems: [ | |||
| { | |||
| key: CommonTabKeys.Public, | |||
| @@ -123,13 +112,10 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = { | |||
| prefix: 'model', | |||
| deleteModalTitle: '确定删除该条模型实例吗?', | |||
| addBtnTitle: '新建模型', | |||
| idParamKey: 'models_id', | |||
| uploadAction: '/api/mmp/models/upload', | |||
| uploadAction: '/api/mmp/newmodel/upload', | |||
| uploadAccept: undefined, | |||
| downloadAllAction: '/api/mmp/models/downloadAllFiles', | |||
| downloadSingleAction: '/api/mmp/models/download_model', | |||
| infoTypePropertyName: 'model_type_name', | |||
| infoTagPropertyName: 'model_tag_name', | |||
| downloadAllAction: '/api/mmp/newmodel/downloadAllFiles', | |||
| downloadSingleAction: '/api/mmp/newmodel/downloadSingleFile', | |||
| }, | |||
| }; | |||
| @@ -141,36 +127,53 @@ export type CategoryData = { | |||
| path: string; | |||
| }; | |||
| // 资源数据 | |||
| // 数据集、模型列表数据 | |||
| export type ResourceData = { | |||
| id: number; | |||
| name: string; | |||
| description: string; | |||
| create_by: string; | |||
| update_time: string; | |||
| available_range: number; | |||
| model_type_name?: string; | |||
| model_tag_name?: string; | |||
| dataset_type_name?: string; | |||
| dataset_tag_name?: string; | |||
| identifier: string; | |||
| owner: string; | |||
| version: string; | |||
| is_public: boolean; | |||
| description?: string; | |||
| create_by?: string; | |||
| update_time?: string; | |||
| time_ago?: string; | |||
| version_desc?: string; | |||
| usage?: string; | |||
| relative_paths?: string; | |||
| // 数据集 | |||
| data_type?: string; // 数据集分类 | |||
| data_tag?: string; // 研究方向 | |||
| processing_code?: string; // 处理代码 | |||
| dataset_source?: string; // 数据来源 | |||
| dataset_version_vos: ResourceFileData[]; | |||
| // 模型 | |||
| model_type?: string; // 模型框架 | |||
| model_tag?: string; // 模型能力 | |||
| image?: string; // 训练镜像 | |||
| code?: string; // 训练镜像 | |||
| train_datasets?: string[]; // 训练数据集 | |||
| test_datasets?: string[]; // 测试数据集 | |||
| params?: Record<string, string>; // 参数 | |||
| metrics?: Record<string, string>; // 指标 | |||
| train_task?: string; // 训练任务 | |||
| model_source?: string; // 模型来源 | |||
| model_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; | |||
| }; | |||
| @@ -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 <ResourceIntro resourceType={ResourceType.Dataset} />; | |||
| function DatasetInfo() { | |||
| return <ResourceInfo resourceType={ResourceType.Dataset} />; | |||
| } | |||
| export default DatasetIntro; | |||
| export default DatasetInfo; | |||
| @@ -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() { | |||
| <Form | |||
| name="editor-create" | |||
| labelCol={{ flex: '100px' }} | |||
| wrapperCol={{ flex: 1 }} | |||
| labelAlign="left" | |||
| form={form} | |||
| initialValues={{ computing_resource: ComputingResourceType.GPU }} | |||
| @@ -2,10 +2,10 @@ import editExperimentIcon from '@/assets/img/edit-experiment.png'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { type ResourceData } from '@/pages/Dataset/config'; | |||
| import { | |||
| addModelsVersionDetail, | |||
| addModelVersion, | |||
| exportModelReq, | |||
| getModelList, | |||
| getModelVersionsById, | |||
| getModelVersionList, | |||
| } from '@/services/dataset'; | |||
| import { to } from '@/utils/promise'; | |||
| import { InfoCircleOutlined } from '@ant-design/icons'; | |||
| @@ -85,7 +85,7 @@ function ExportModelModal({ path, onOk, ...rest }: ExportModelModalProps) { | |||
| // 获取模型版本列表 | |||
| const getModelVersions = async (id: number) => { | |||
| const [res] = await to(getModelVersionsById(id)); | |||
| const [res] = await to(getModelVersionList(id)); | |||
| if (res && res.data) { | |||
| setVersions(res.data); | |||
| } | |||
| @@ -118,7 +118,7 @@ function ExportModelModal({ path, onOk, ...rest }: ExportModelModalProps) { | |||
| // 创建模型版本 | |||
| const createModelVersion = async (params: CreateModelVersionParams[]) => { | |||
| const [res] = await to(addModelsVersionDetail(params)); | |||
| const [res] = await to(addModelVersion(params)); | |||
| if (res) { | |||
| onOk(); | |||
| } | |||
| @@ -8,6 +8,7 @@ import DateTableCell from '@/components/DateTableCell'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { MirrorVersionStatus } from '@/enums'; | |||
| import { useDomSize } from '@/hooks'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { | |||
| @@ -36,7 +37,7 @@ import { useEffect, useMemo, useState } from 'react'; | |||
| import MirrorStatusCell from '../components/MirrorStatusCell'; | |||
| import styles from './index.less'; | |||
| type MirrorInfoData = { | |||
| export type MirrorInfoData = { | |||
| name?: string; | |||
| description?: string; | |||
| version_count?: string; | |||
| @@ -44,13 +45,14 @@ type MirrorInfoData = { | |||
| image_type?: number; | |||
| }; | |||
| type MirrorVersionData = { | |||
| export type MirrorVersionData = { | |||
| id: number; | |||
| version: string; | |||
| url: string; | |||
| status: string; | |||
| status: MirrorVersionStatus; | |||
| file_size: string; | |||
| create_time: string; | |||
| tag_name: string; | |||
| }; | |||
| function MirrorInfo() { | |||
| @@ -5,13 +5,11 @@ | |||
| */ | |||
| import { useEffectWhen } from '@/hooks'; | |||
| import { ResourceVersionData } from '@/pages/Dataset/config'; | |||
| import { getModelAtlasReq } from '@/services/dataset/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { to } from '@/utils/promise'; | |||
| import G6, { G6GraphEvent, Graph, INode } from '@antv/g6'; | |||
| // @ts-ignore | |||
| import { Flex, Select } from 'antd'; | |||
| import { Flex } from 'antd'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| import GraphLegend from '../GraphLegend'; | |||
| import NodeTooltips from '../NodeTooltips'; | |||
| @@ -29,7 +27,7 @@ import { | |||
| type modeModelEvolutionProps = { | |||
| resourceId: number; | |||
| versionList: ResourceVersionData[]; | |||
| identifier: string; | |||
| version?: string; | |||
| isActive: boolean; | |||
| onVersionChange: (version: string) => void; | |||
| @@ -38,7 +36,7 @@ type modeModelEvolutionProps = { | |||
| let graph: Graph; | |||
| function ModelEvolution({ | |||
| resourceId, | |||
| versionList, | |||
| identifier, | |||
| version, | |||
| isActive, | |||
| onVersionChange, | |||
| @@ -217,7 +215,8 @@ function ModelEvolution({ | |||
| // 获取模型依赖 | |||
| const getModelAtlas = async () => { | |||
| const params = { | |||
| current_model_id: resourceId, | |||
| id: resourceId, | |||
| identifier, | |||
| version, | |||
| }; | |||
| const [res] = await to(getModelAtlasReq(params)); | |||
| @@ -250,15 +249,6 @@ function ModelEvolution({ | |||
| return ( | |||
| <div className={styles['model-evolution']}> | |||
| <Flex align="center" className={styles['model-evolution__top']}> | |||
| <span style={{ marginRight: '10px' }}>版本号:</span> | |||
| <Select | |||
| placeholder="请选择版本号" | |||
| style={{ width: '160px', marginRight: '20px' }} | |||
| value={version} | |||
| allowClear | |||
| onChange={onVersionChange} | |||
| options={versionList} | |||
| /> | |||
| <GraphLegend style={{ marginRight: 0, marginLeft: 'auto' }}></GraphLegend> | |||
| </Flex> | |||
| <div className={styles['model-evolution__graph']} id="canvas" ref={graphRef}></div> | |||
| @@ -1,4 +1,4 @@ | |||
| import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceIntro'; | |||
| import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { useNavigate } from '@umijs/max'; | |||
| import { ModelDepsData, NodeType, ProjectDependency, TrainDataset } from '../ModelEvolution/utils'; | |||
| @@ -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 ModelIntro() { | |||
| return <ResourceIntro resourceType={ResourceType.Model} />; | |||
| function ModelInfo() { | |||
| return <ResourceInfo resourceType={ResourceType.Model} />; | |||
| } | |||
| export default ModelIntro; | |||
| export default ModelInfo; | |||
| @@ -7,12 +7,12 @@ import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import ResourceSelect, { | |||
| requiredValidator, | |||
| ResourceSelectorType, | |||
| type ParameterInputObject, | |||
| } from '@/components/ResourceSelect'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { ResourceSelectorType } from '@/pages/Pipeline/components/ResourceSelectorModal'; | |||
| import { | |||
| createModelDeploymentReq, | |||
| restartModelDeploymentReq, | |||
| @@ -67,7 +67,7 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| <KFModal | |||
| {...rest} | |||
| title="选择代码配置" | |||
| image={require('@/assets/img/edit-experiment.png')} | |||
| image={require('@/assets/img/modal-code-config.png')} | |||
| width={920} | |||
| footer={null} | |||
| destroyOnClose | |||
| @@ -245,6 +245,10 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| // 获取选择数据集、模型后面按钮 icon | |||
| const getSelectBtnIcon = (item: { item_type: string }) => { | |||
| const type = item.item_type; | |||
| if (type === 'code') { | |||
| return <KFIcon type="icon-xuanzedaimapeizhi" />; | |||
| } | |||
| let selectorType: ResourceSelectorType; | |||
| if (type === 'dataset') { | |||
| selectorType = ResourceSelectorType.Dataset; | |||
| @@ -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, | |||
| getModelInfo, | |||
| getModelList, | |||
| getModelVersionIdList, | |||
| getModelVersionsById, | |||
| getModelVersionList, | |||
| } 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,344 @@ 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<any>; // 获取资源列表 | |||
| getVersions: (params: any) => Promise<any>; // 获取资源版本列表 | |||
| getFiles: (params: any) => Promise<any>; // 获取资源文件列表 | |||
| 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<any> => { | |||
| 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<any>; // 获取资源列表 | |||
| // getVersions: (params: any) => Promise<any>; // 获取资源版本列表 | |||
| // getFiles: (params: any) => Promise<any>; // 获取资源文件列表 | |||
| // 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, SelectorTypeInfo> = { | |||
| // [ResourceSelectorType.Model]: { | |||
| // getList: getModelList, | |||
| // getVersions: getModelVersionList, | |||
| // 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<any>; // 获取资源列表 | |||
| getVersions: (parentKey: string, parentNode: any) => Promise<any>; // 获取资源版本列表 | |||
| getFiles: (parentKey: string, parentNode: any) => Promise<any>; // 获取资源文件列表 | |||
| 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 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 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 res = await getModelVersionList(pick(parentNode, ['owner', 'identifier'])); | |||
| if (res && res.data) { | |||
| const list = res.data; | |||
| return convertDatasetVersionToTreeData(key, parentNode, list); | |||
| } else { | |||
| return Promise.reject('获取模型版本列表失败'); | |||
| } | |||
| } | |||
| async getFiles(_parentKey: string, parentNode: ResourceData & ResourceVersionData) { | |||
| const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version']); | |||
| const res = await getModelInfo(params); | |||
| if (res && res.data) { | |||
| const path = res.data.relative_paths || ''; | |||
| const list = res.data.model_version_vos || []; | |||
| return { | |||
| 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}`, | |||
| url: url, | |||
| file_name: `${url}`, | |||
| }, | |||
| ], | |||
| }, | |||
| }); | |||
| }; | |||
| }; | |||
| } | |||
| } | |||
| export const selectorTypeConfig: Record<ResourceSelectorType, SelectorTypeInfo> = { | |||
| [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(), | |||
| }; | |||
| @@ -34,6 +34,11 @@ | |||
| border-bottom: 1px solid @border-color-secondary; | |||
| border-radius: 0; | |||
| } | |||
| &__tree-title { | |||
| display: inline-block; | |||
| .singleLine(); | |||
| } | |||
| } | |||
| &__right { | |||
| @@ -6,34 +6,25 @@ | |||
| import KFModal from '@/components/KFModal'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { ResourceFileData } from '@/pages/Dataset/config'; | |||
| import { to } from '@/utils/promise'; | |||
| 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 | |||
| }; | |||
| export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> { | |||
| type: ResourceSelectorType; // 数据集\模型\镜像 | |||
| defaultExpandedKeys?: React.Key[]; | |||
| @@ -44,39 +35,8 @@ export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> { | |||
| type TreeRef = GetRef<typeof Tree<TreeDataNode>>; | |||
| // 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 +51,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, | |||
| @@ -112,11 +72,11 @@ function ResourceSelectorModal({ | |||
| const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([]); | |||
| const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]); | |||
| const [originTreeData, setOriginTreeData] = useState<TreeDataNode[]>([]); | |||
| const [files, setFiles] = useState<ResourceFile[]>([]); | |||
| const [files, setFiles] = useState<ResourceFileData[]>([]); | |||
| 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<TreeRef>(null); | |||
| const config = selectorTypeConfig[type]; | |||
| @@ -140,18 +100,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 +113,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 +136,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 +148,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,14 +163,15 @@ 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 { | |||
| setVersionPath(''); | |||
| setFiles([]); | |||
| } | |||
| }; | |||
| @@ -229,10 +180,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 +194,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 +221,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, | |||
| @@ -324,13 +276,23 @@ function ResourceSelectorModal({ | |||
| expandedKeys={expandedKeys} | |||
| onExpand={onExpand} | |||
| checkable | |||
| titleRender={(nodeData) => { | |||
| return ( | |||
| <span | |||
| className={styles['model-selector__left__tree-title']} | |||
| style={{ width: nodeData.isLeaf ? '370px' : '420px' }} | |||
| > | |||
| {nodeData.title as string} | |||
| </span> | |||
| ); | |||
| }} | |||
| /> | |||
| </div> | |||
| <div className={styles['model-selector__right']}> | |||
| <div className={styles['model-selector__right__title']}>{fileTitle}</div> | |||
| <div className={styles['model-selector__right__files']}> | |||
| {files.map((v) => ( | |||
| <div key={v.id} className={styles['model-selector__right__files__file']}> | |||
| <div key={v.url} className={styles['model-selector__right__files__file']}> | |||
| {v.file_name} | |||
| </div> | |||
| ))} | |||
| @@ -129,7 +129,7 @@ const AuthUserTableList: React.FC = () => { | |||
| { | |||
| title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />, | |||
| dataIndex: 'option', | |||
| width: '60px', | |||
| width: '160px', | |||
| valueType: 'option', | |||
| render: (_, record) => [ | |||
| <Button | |||
| @@ -111,10 +111,7 @@ const DataScopeForm: React.FC<DataScopeFormProps> = (props) => { | |||
| grid={true} | |||
| layout="horizontal" | |||
| onFinish={handleFinish} | |||
| initialValues={{ | |||
| login_password: '', | |||
| confirm_password: '', | |||
| }} | |||
| submitter={false} | |||
| {...formLayout} | |||
| size="large" | |||
| labelAlign="right" | |||
| @@ -203,8 +200,9 @@ const DataScopeForm: React.FC<DataScopeFormProps> = (props) => { | |||
| })} | |||
| required={dataScopeType === '1'} | |||
| hidden={dataScopeType !== '1'} | |||
| style={{ width: '100%', padding: '0 4px' }} | |||
| > | |||
| <Row gutter={[16, 16]}> | |||
| <Row gutter={[16, 16]} style={{ marginTop: '10px' }}> | |||
| <Col md={24}> | |||
| <Checkbox.Group | |||
| options={[ | |||
| @@ -235,7 +233,7 @@ const DataScopeForm: React.FC<DataScopeFormProps> = (props) => { | |||
| } | |||
| }} | |||
| onExpand={(expandedKeys: Key[]) => { | |||
| setDeptTreeExpandKey(deptTreeExpandKey.concat(expandedKeys)); | |||
| setDeptTreeExpandKey(expandedKeys); | |||
| }} | |||
| /> | |||
| </Col> | |||
| @@ -0,0 +1,12 @@ | |||
| .user-selector-modal { | |||
| :global { | |||
| // 输入框高度为46px | |||
| .ant-input-affix-wrapper { | |||
| padding-top: 4px !important; | |||
| padding-bottom: 4px !important; | |||
| .ant-input { | |||
| height: 22px !important; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -10,6 +10,7 @@ import { | |||
| } from '@ant-design/pro-components'; | |||
| import { FormattedMessage, useIntl } from '@umijs/max'; | |||
| import React, { useEffect, useRef, useState } from 'react'; | |||
| import styles from './UserSelectorModal.less'; | |||
| /* * | |||
| * | |||
| * @author whiteshader@163.com | |||
| @@ -90,7 +91,7 @@ const UserSelectorModal: React.FC<DataScopeFormProps> = (props) => { | |||
| return ( | |||
| <KFModal | |||
| width={800} | |||
| width={920} | |||
| title={intl.formatMessage({ | |||
| id: 'system.role.auth.user', | |||
| defaultMessage: '选择用户', | |||
| @@ -99,6 +100,7 @@ const UserSelectorModal: React.FC<DataScopeFormProps> = (props) => { | |||
| destroyOnClose | |||
| onOk={handleOk} | |||
| onCancel={handleCancel} | |||
| className={styles['user-selector-modal']} | |||
| > | |||
| <ProTable<API.System.User> | |||
| headerTitle={intl.formatMessage({ | |||
| @@ -34,7 +34,7 @@ const AuthRoleForm: React.FC<AuthRoleFormProps> = (props) => { | |||
| return ( | |||
| <KFModal | |||
| width={640} | |||
| width={680} | |||
| title={intl.formatMessage({ | |||
| id: 'system.user.auth.role', | |||
| defaultMessage: '分配角色', | |||
| @@ -50,10 +50,10 @@ const AuthRoleForm: React.FC<AuthRoleFormProps> = (props) => { | |||
| grid={true} | |||
| layout="horizontal" | |||
| onFinish={handleFinish} | |||
| initialValues={{ | |||
| login_password: '', | |||
| confirm_password: '', | |||
| }} | |||
| submitter={false} | |||
| size="large" | |||
| labelAlign="right" | |||
| autoComplete="off" | |||
| > | |||
| <ProFormSelect | |||
| name="roleIds" | |||
| @@ -39,7 +39,7 @@ const UpdateForm: React.FC<UpdateFormProps> = (props) => { | |||
| return ( | |||
| <KFModal | |||
| width={640} | |||
| width={680} | |||
| title={intl.formatMessage({ | |||
| id: 'system.user.reset.password', | |||
| defaultMessage: '密码重置', | |||
| @@ -58,8 +58,12 @@ const UpdateForm: React.FC<UpdateFormProps> = (props) => { | |||
| password: '', | |||
| confirm_password: '', | |||
| }} | |||
| submitter={false} | |||
| size="large" | |||
| labelAlign="right" | |||
| autoComplete="off" | |||
| > | |||
| <p>请输入用户{props.values.userName}的新密码!</p> | |||
| <p>请输入用户 {props.values.userName} 的新密码!</p> | |||
| <ProFormText.Password | |||
| name="password" | |||
| label="登录密码" | |||
| @@ -62,6 +62,8 @@ const UserForm: React.FC<UserFormProps> = (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<UserFormProps> = (props) => { | |||
| colProps={{ md: 12, xl: 12 }} | |||
| rules={[{ required: true, message: '请选择角色!' }]} | |||
| /> | |||
| <ProFormText | |||
| name="gitLinkUsername" | |||
| label="Git 用户名" | |||
| placeholder="请输入 Git 用户名" | |||
| colProps={{ xs: 24, md: 12, xl: 12 }} | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入 Git 用户名!', | |||
| }, | |||
| ]} | |||
| /> | |||
| <ProFormText.Password | |||
| name="gitLinkPassword" | |||
| label="Git 密码" | |||
| placeholder="请输入 Git 密码" | |||
| colProps={{ xs: 24, md: 12, xl: 12 }} | |||
| fieldProps={{ | |||
| autoComplete: 'new-password', | |||
| }} | |||
| rules={props.values.userId ? [] : [{ required: true, message: '请输入 Git 密码!' }]} | |||
| /> | |||
| <ProFormTextArea | |||
| name="remark" | |||
| label={intl.formatMessage({ | |||
| @@ -1,10 +1,20 @@ | |||
| import missingPage from '@/assets/img/missing-back.png'; | |||
| import KFEmpty, { EmptyType } from '@/components/KFEmpty'; | |||
| import { useNavigate } from '@umijs/max'; | |||
| const MissingPage = () => ( | |||
| <div style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}> | |||
| <img src={missingPage} style={{ width: '575px', margin: '278px 0 44px 0' }} alt="" /> | |||
| <span style={{ color: '#575757', fontSize: '16px' }}>页面开发中,敬请期待......</span> | |||
| </div> | |||
| ); | |||
| const MissingPage = () => { | |||
| const navigate = useNavigate(); | |||
| return ( | |||
| <KFEmpty | |||
| style={{ height: '100%' }} | |||
| type={EmptyType.Developing} | |||
| title="敬请期待~" | |||
| content={'很抱歉,您访问的正在开发中,\n请耐心等待。'} | |||
| hasFooter={true} | |||
| buttonTitle="返回首页" | |||
| onRefresh={() => navigate('/')} | |||
| ></KFEmpty> | |||
| ); | |||
| }; | |||
| export default MissingPage; | |||
| @@ -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. | |||
| @@ -25,6 +26,7 @@ const popupError = (error: string, skipErrorHandler: boolean | undefined = false | |||
| * @doc https://umijs.org/docs/max/request#配置 | |||
| */ | |||
| export const requestConfig: RequestConfig = { | |||
| timeout: 120 * 1000, | |||
| requestInterceptors: [ | |||
| (url: string, options: AxiosRequestConfig) => { | |||
| const headers = options.headers ?? {}; | |||
| @@ -36,12 +38,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 +67,7 @@ export const requestConfig: RequestConfig = { | |||
| } | |||
| }, | |||
| (error: Error) => { | |||
| Loading.hide(); | |||
| popupError(error.message ?? '请求失败'); | |||
| return Promise.reject(error); | |||
| }, | |||
| @@ -1,140 +1,153 @@ | |||
| import { request } from '@umijs/max'; | |||
| // 分页查询数据集 | |||
| export function getDatasetList(params) { | |||
| return request(`/api/mmp/dataset`, { | |||
| // 查询数据集、模型分类 | |||
| export function getAssetIcon(params) { | |||
| return request(`/api/mmp/assetIcon`, { | |||
| method: 'GET', | |||
| params, | |||
| }); | |||
| } | |||
| // 分页查询模型 | |||
| export function getModelList(params) { | |||
| return request(`/api/mmp/models`, { | |||
| // ----------------------------数据集--------------------------------- | |||
| // 分页查询数据集列表 | |||
| export function getDatasetList(params) { | |||
| return request(`/api/mmp/newdataset/queryDatasets`, { | |||
| method: 'GET', | |||
| params, | |||
| }); | |||
| } | |||
| // 新增数据集 | |||
| export function addDatesetAndVesion(data) { | |||
| return request(`/api/mmp/dataset/addDatasetAndVersion`, { | |||
| method: 'POST', | |||
| headers: { | |||
| 'Content-Type': 'application/json;charset=UTF-8', | |||
| }, | |||
| data, | |||
| // 查询数据集详情 | |||
| export function getDatasetInfo(params) { | |||
| return request(`/api/mmp/newdataset/getDatasetDetail`, { | |||
| method: 'GET', | |||
| params, | |||
| }); | |||
| } | |||
| // 新增模型 | |||
| export function addModel(data) { | |||
| return request(`/api/mmp/models/addModelAndVersion`, { | |||
| // 新增数据集 | |||
| export function addDataset(data) { | |||
| return request(`/api/mmp/newdataset/addDatasetAndVersion`, { | |||
| method: 'POST', | |||
| headers: { | |||
| 'Content-Type': 'application/json;charset=UTF-8', | |||
| }, | |||
| data, | |||
| }); | |||
| } | |||
| // 查询数据集简介 | |||
| export function getDatasetById(id) { | |||
| return request(`/api/mmp/dataset/${id}`, { | |||
| method: 'GET', | |||
| // 删除数据集 | |||
| export function deleteDataset(params) { | |||
| return request(`/api/mmp/newdataset/deleteDataset`, { | |||
| method: 'DELETE', | |||
| params, | |||
| }); | |||
| } | |||
| // 查询左侧列表 | |||
| export function getAssetIcon(params) { | |||
| return request(`/api/mmp/assetIcon`, { | |||
| // 查询数据集版本列表 | |||
| export function getDatasetVersionList(params) { | |||
| return request(`/api/mmp/newdataset/getVersionList`, { | |||
| method: 'GET', | |||
| params, | |||
| }); | |||
| } | |||
| // 查询模型简介 | |||
| export function getModelById(id) { | |||
| return request(`/api/mmp/models/${id}`, { | |||
| method: 'GET', | |||
| // 新增数据集版本 | |||
| export function addDatasetVersion(data) { | |||
| return request(`/api/mmp/newdataset/addVersion`, { | |||
| method: 'POST', | |||
| data, | |||
| }); | |||
| } | |||
| // 查询数据版本集 | |||
| export function getDatasetVersionsById(id) { | |||
| return request(`/api/mmp/dataset/versions/${id}`, { | |||
| // 下载数据集所有文件 | |||
| export function downloadAllFiles(params) { | |||
| return request(`/api/mmp/newdataset/downloadAllFiles`, { | |||
| method: 'GET', | |||
| params | |||
| }); | |||
| } | |||
| // 查询模型版本集 | |||
| export function getModelVersionsById(id) { | |||
| return request(`/api/mmp/models/versions/${id}`, { | |||
| // 下载数据集单个文件 | |||
| export function downloadSingleFile(params) { | |||
| return request(`/api/mmp/newdataset/downloadSinggerFile`, { | |||
| method: 'GET', | |||
| params, | |||
| }); | |||
| } | |||
| // 分页查询数据集 | |||
| export function getDatasetVersionIdList(params) { | |||
| return request(`/api/mmp/datasetVersion/versions`, { | |||
| method: 'GET', | |||
| // 删除数据集版本 | |||
| export function deleteDatasetVersion(params) { | |||
| return request(`/api/mmp/newdataset/deleteDatasetVersion`, { | |||
| method: 'DELETE', | |||
| params, | |||
| }); | |||
| } | |||
| // 根据版本查询模型 | |||
| export function getModelVersionIdList(params) { | |||
| return request(`/api/mmp/modelsVersion/versions`, { | |||
| // ----------------------------模型--------------------------------- | |||
| // 分页查询模型列表 | |||
| export function getModelList(params) { | |||
| return request(`/api/mmp/newmodel/queryModels`, { | |||
| method: 'GET', | |||
| params, | |||
| }); | |||
| } | |||
| // 删除数据集 | |||
| export function deleteDatasetVersion(params) { | |||
| return request(`/api/mmp/datasetVersion/deleteVersion`, { | |||
| method: 'DELETE', | |||
| params, | |||
| // 新增模型 | |||
| export function addModel(data) { | |||
| return request(`/api/mmp/newmodel/addModel`, { | |||
| method: 'POST', | |||
| data, | |||
| }); | |||
| } | |||
| // 删除模型 | |||
| export function deleteModelVersion(params) { | |||
| return request(`/api/mmp/modelsVersion/deleteVersion`, { | |||
| export function deleteModel(params) { | |||
| return request(`/api/mmp/newmodel/delete`, { | |||
| 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`, { | |||
| method: 'POST', | |||
| headers: { | |||
| 'Content-Type': 'application/json;charset=UTF-8', | |||
| }, | |||
| data, | |||
| // 查询模型详情 | |||
| export function getModelInfo(params) { | |||
| return request(`/api/mmp/newmodel/getModelDetail`, { | |||
| method: 'GET', | |||
| params, | |||
| }); | |||
| } | |||
| // 下载数据集 | |||
| export function exportDataset(id) { | |||
| return request(`/api/mmp/dataset/download/${id}`, { | |||
| // 查询模型版本列表 | |||
| export function getModelVersionList(params) { | |||
| return request(`/api/mmp/newmodel/getVersionList`, { | |||
| method: 'GET', | |||
| params, | |||
| }); | |||
| } | |||
| // 删除模型集 | |||
| export function deleteModel(id) { | |||
| return request(`/api/mmp/models/${id}`, { | |||
| method: 'DELETE', | |||
| // 新增模型版本 | |||
| export function addModelVersion(data) { | |||
| return request(`/api/mmp/newmodel/addVersion`, { | |||
| method: 'POST', | |||
| data, | |||
| }); | |||
| } | |||
| // 删除数据集 | |||
| export function deleteDataset(id) { | |||
| return request(`/api/mmp/dataset/${id}`, { | |||
| // 删除模型版本 | |||
| export function deleteModelVersion(params) { | |||
| return request(`/api/mmp/newmodel/deleteVersion`, { | |||
| method: 'DELETE', | |||
| params, | |||
| }); | |||
| } | |||
| // 获取模型依赖 | |||
| export function getModelAtlasReq(data) { | |||
| return request(`/api/mmp/modelDependency/queryModelAtlas`, { | |||
| method: 'POST', | |||
| data | |||
| export function getModelAtlasReq(params) { | |||
| return request(`/api/mmp/newmodel/getModelDependencyTree`, { | |||
| method: 'GET', | |||
| params | |||
| }); | |||
| } | |||
| @@ -19,6 +19,8 @@ declare namespace API.System { | |||
| updateBy: string; | |||
| updateTime: Date; | |||
| remark: string; | |||
| gitLinkUsername?: string; | |||
| gitLinkPassword?: string; | |||
| } | |||
| export interface UserListParams { | |||
| @@ -190,3 +190,13 @@ export const fittingString = (str: string, maxWidth: number, fontSize: number) = | |||
| }); | |||
| return res; | |||
| }; | |||
| /** | |||
| * Checks if a given string is empty, undefined, or null. | |||
| * | |||
| * @param {any} str - the string to be checked | |||
| * @return {boolean} true if the string is empty, undefined, or null, false otherwise | |||
| */ | |||
| export const isEmptyString = (str: any): boolean => { | |||
| return str === '' || str === undefined || str === null; | |||
| }; | |||
| @@ -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<typeof setTimeout> | 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<typeof setTimeout>; | |||
| let renderTimeoutId: ReturnType<typeof setTimeout>; | |||
| 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 = <KFSpin {...spinProps} />; | |||
| root.render( | |||
| <ConfigProvider | |||
| prefixCls={rootPrefixCls} | |||
| @@ -45,21 +53,41 @@ export class Loading { | |||
| </ConfigProvider>, | |||
| ); | |||
| }); | |||
| } | |||
| }; | |||
| 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)); | |||
| } | |||
| } | |||
| @@ -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; | |||