diff --git a/.gitignore b/.gitignore index 54c883c5..41d080fe 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ mvnw /react-ui/types/tsconfig.tsbuildinfo /react-ui/storybook-static /react-ui/.storybook/scripts +/react-ui/dist.zip diff --git a/react-ui/src/components/ParameterSelect/config.tsx b/react-ui/src/components/ParameterSelect/config.tsx index 722c174c..59d984b0 100644 --- a/react-ui/src/components/ParameterSelect/config.tsx +++ b/react-ui/src/components/ParameterSelect/config.tsx @@ -1,8 +1,9 @@ -import { filterResourceStandard, resourceFieldNames } from '@/hooks/useComputingResource'; import { DatasetData, ModelData } from '@/pages/Dataset/config'; import { ServiceData } from '@/pages/ModelDeployment/types'; import { getDatasetList, getModelList } from '@/services/dataset/index.js'; import { getServiceListReq } from '@/services/modelDeployment'; +import type { JCCResourceImage, JCCResourceStandard, JCCResourceType } from '@/state/jcdResource'; +import { filterResourceStandard, resourceFieldNames } from '@/state/systemResource'; import { type SelectProps } from 'antd'; export type SelectPropsConfig = { @@ -10,12 +11,21 @@ export type SelectPropsConfig = { fieldNames?: SelectProps['fieldNames']; // 下拉数据字段 optionFilterProp?: SelectProps['optionFilterProp']; // 过滤字段名 filterOption?: SelectProps['filterOption']; // 过滤函数 - getValue: (value: any) => string | number; - getLabel: (value: any) => string; isObjectValue: boolean; // value 是对象 + getValue?: (value: any) => string | number; // 对象类型时,获取其值 + getLabel?: (value: any) => string; // 对象类型时,获取其 label }; -export const paramSelectConfig: Record = { +export type ParameterSelectDataType = + | 'dataset' + | 'model' + | 'service' + | 'resource' + | 'remote-image' + | 'remote-resource-type' + | 'remote-resource'; + +export const paramSelectConfig: Record = { dataset: { getOptions: async () => { const res = await getDatasetList({ @@ -72,14 +82,44 @@ export const paramSelectConfig: Record = { resource: { fieldNames: resourceFieldNames, filterOption: filterResourceStandard as SelectProps['filterOption'], - // 不会用到 - getValue: () => { - return ''; + isObjectValue: false, + }, + 'remote-resource-type': { + optionFilterProp: 'label', + isObjectValue: false, + getValue: (value: JCCResourceType) => { + return value.value; }, - // 不会用的 - getLabel: () => { - return ''; + getLabel: (value: JCCResourceType) => { + return value.label; }, - isObjectValue: false, + }, + 'remote-image': { + optionFilterProp: 'label', + getValue: (value: JCCResourceImage) => { + return value.imageID; + }, + getLabel: (value: JCCResourceImage) => { + return value.name; + }, + isObjectValue: true, + }, + 'remote-resource': { + optionFilterProp: 'label', + getValue: (value: JCCResourceStandard) => { + return value.id; + }, + getLabel: (value: JCCResourceStandard) => { + const cpu = value.baseResourceSpecs.find((v) => v.type === 'CPU'); + const ram = value.baseResourceSpecs.find((v) => v.type === 'MEMORY' && v.name === 'RAM'); + const vram = value.baseResourceSpecs.find((v) => v.type === 'MEMORY' && v.name === 'VRAM'); + const cpuText = cpu ? `CPU:${cpu.availableValue}, ` : ''; + const ramText = ram ? `内存: ${ram.availableValue}${ram.availableUnit?.toUpperCase()}` : ''; + const vramText = vram + ? `(显存${vram.availableValue}${vram.availableUnit?.toUpperCase()})` + : ''; + return `${value.type}: ${value.availableCount}*${value.name}${vramText}, ${cpuText}${ramText}`; + }, + isObjectValue: true, }, }; diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index 3c1ab102..4beddf7f 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -4,19 +4,25 @@ * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 */ -import { useComputingResource } from '@/hooks/useComputingResource'; +import jccResourceState from '@/state/jcdResource'; +import systemResourceState, { getSystemResources } from '@/state/systemResource'; import { to } from '@/utils/promise'; +import { useSnapshot } from '@umijs/max'; import { Select, type SelectProps } from 'antd'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import FormInfo from '../FormInfo'; -import { paramSelectConfig } from './config'; +import { paramSelectConfig, type ParameterSelectDataType } from './config'; + +export { type ParameterSelectDataType }; export type ParameterSelectObject = { value: any; [key: string]: any; }; -export type ParameterSelectDataType = 'dataset' | 'model' | 'service' | 'resource'; +type SelectOptions = SelectProps['options']; + +const identityFunc = (value: any) => value; export interface ParameterSelectProps extends SelectProps { /** 类型 */ @@ -25,8 +31,6 @@ export interface ParameterSelectProps extends SelectProps { display?: boolean; /** 值,支持对象,对象必须包含 value */ value?: string | ParameterSelectObject; - /** 用于流水线, 流水线资源规格要求 id 为字符串 */ - isPipeline?: boolean; /** 修改后回调 */ onChange?: (value: string | ParameterSelectObject) => void; } @@ -36,15 +40,14 @@ function ParameterSelect({ dataType, display = false, value, - // isPipeline = false, onChange, ...rest }: ParameterSelectProps) { - const [options, setOptions] = useState([]); + const [options, setOptions] = useState([]); const propsConfig = paramSelectConfig[dataType]; const { - getLabel, - getValue, + getLabel = identityFunc, + getValue = identityFunc, getOptions, filterOption, fieldNames, @@ -55,29 +58,45 @@ function ParameterSelect({ // 数据集、模型、服务,对象转换成 json 字符串 const valueText = typeof selectValue === 'object' && selectValue !== null ? getValue(selectValue) : selectValue; - const [resourceStandardList] = useComputingResource(); - // const computingResource = isPipeline - // ? resourceStandardList.map((v) => ({ - // ...v, - // id: String(v.id), - // })) - // : resourceStandardList; + const jccResourceSnap = useSnapshot(jccResourceState); + const { getResourceTypes } = jccResourceSnap; + const systemResourceSnap = useSnapshot(systemResourceState); const objectOptions = useMemo(() => { - return options?.map((v) => ({ - label: getLabel(v), - value: getValue(v), - })); - }, [getLabel, getValue, options]); + return dataType === 'remote-resource-type' + ? jccResourceSnap.types + : dataType === 'remote-image' + ? jccResourceSnap.images + : dataType === 'remote-resource' + ? jccResourceSnap.resources + : options; + }, [dataType, options, jccResourceSnap.types, jccResourceSnap.images, jccResourceSnap.resources]); + + // 将对象类型转换为 Select Options + const converObjectToOptions = useCallback( + (v: any) => { + return { + label: getLabel(v), + value: getValue(v), + }; + }, + [getLabel, getValue], + ); + + // 数据集、模型、服务获取数据后,进行转换 + const objectSelectOptions = useMemo(() => { + return objectOptions?.map(converObjectToOptions); + }, [converObjectToOptions, objectOptions]); + // 快速得到选中的对象 const valueMap = useMemo(() => { const map = new Map(); - options?.forEach((v) => { + objectOptions?.forEach((v) => { map.set(getValue(v), v); }); return map; - }, [options, getValue]); + }, [objectOptions, getValue]); useEffect(() => { // 获取下拉数据 @@ -87,13 +106,18 @@ function ParameterSelect({ if (res) { setOptions(res); } + } else if (dataType === 'remote-resource-type') { + getResourceTypes(); + } else if (dataType === 'resource') { + getSystemResources(); } }; - getSelectOptions(); - }, [getOptions]); + }, [getOptions, dataType, getResourceTypes]); - const selectOptions = dataType === 'resource' ? resourceStandardList : objectOptions; + const selectOptions = ( + dataType === 'resource' ? systemResourceSnap.resources : objectSelectOptions + ) as SelectOptions; const handleChange = (text: string) => { // 数据集、模型、服务,转换成对象 diff --git a/react-ui/src/hooks/useComputingResource.ts b/react-ui/src/hooks/useComputingResource.ts index b2239247..9eb5aa62 100644 --- a/react-ui/src/hooks/useComputingResource.ts +++ b/react-ui/src/hooks/useComputingResource.ts @@ -4,66 +4,90 @@ * @Description: 资源规格 hook */ -import { getComputingResourceReq } from '@/services/pipeline'; -import { ComputingResource } from '@/types'; -import { to } from '@/utils/promise'; -import { type SelectProps } from 'antd'; -import { useCallback, useEffect, useState } from 'react'; +// import { getComputingResourceReq } from '@/services/pipeline'; +// import { ComputingResource } from '@/types'; +// import { to } from '@/utils/promise'; +// import { type SelectProps } from 'antd'; +// import { useCallback, useEffect, useState } from 'react'; -const computingResource: ComputingResource[] = []; +// const computingResource: ComputingResource[] = []; -/** 过滤资源规格 */ -export const filterResourceStandard: SelectProps['filterOption'] = ( - input: string, - option?: ComputingResource, -) => { - return ( - option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? false - ); -}; +// /** 过滤资源规格 */ +// export const filterResourceStandard: SelectProps['filterOption'] = ( +// input: string, +// option?: ComputingResource, +// ) => { +// return ( +// option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? false +// ); +// }; -/** 资源规格字段 */ -export const resourceFieldNames = { - label: 'description', - value: 'id', -}; +// /** 资源规格字段 */ +// export const resourceFieldNames = { +// label: 'description', +// value: 'id', +// }; -/** 获取资源规格 */ -export function useComputingResource() { - const [resourceStandardList, setResourceStandardList] = useState([]); +// /** 获取资源规格 */ +// export function useComputingResource() { +// const [resourceStandardList, setResourceStandardList] = useState([]); - useEffect(() => { - // 获取资源规格列表数据 - const getComputingResource = async () => { - const params = { - page: 0, - size: 1000, - resource_type: '', - }; - const [res] = await to(getComputingResourceReq(params)); - if (res && res.data && Array.isArray(res.data.content)) { - setResourceStandardList(res.data.content); - computingResource.splice(0, computingResource.length, ...res.data.content); - } - }; +// useEffect(() => { +// // 获取资源规格列表数据 +// const getComputingResource = async () => { +// const params = { +// page: 0, +// size: 1000, +// resource_type: '', +// }; +// const [res] = await to(getComputingResourceReq(params)); +// if (res && res.data && Array.isArray(res.data.content)) { +// setResourceStandardList(res.data.content); +// computingResource.splice(0, computingResource.length, ...res.data.content); +// } +// }; + +// if (computingResource.length > 0) { +// setResourceStandardList(computingResource); +// } else { +// getComputingResource(); +// } +// }, []); - if (computingResource.length > 0) { - setResourceStandardList(computingResource); - } else { - getComputingResource(); - } +// // 根据 standard 获取 description +// const getDescription = useCallback( +// (id?: string | number) => { +// if (!id) { +// return undefined; +// } +// return resourceStandardList.find((item) => Number(item.id) === Number(id))?.description; +// }, +// [resourceStandardList], +// ); + +// return [resourceStandardList, getDescription] as const; +// } + +import state, { getSystemResources } from '@/state/systemResource'; +import { useSnapshot } from '@umijs/max'; +import { useCallback, useEffect } from 'react'; + +export const useSystemResource = () => { + useEffect(() => { + getSystemResources(); }, []); - // 根据 standard 获取 description + const snap = useSnapshot(state); + /* 根据 standard 获取 description */ const getDescription = useCallback( (id?: string | number) => { if (!id) { return undefined; } - return resourceStandardList.find((item) => Number(item.id) === Number(id))?.description; + return snap.resources.find((item) => Number(item.id) === Number(id))?.description; }, - [resourceStandardList], + [snap.resources], ); - return [resourceStandardList, getDescription] as const; -} + return getDescription; +}; diff --git a/react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.tsx b/react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.tsx index e0decf8b..82ad98d9 100644 --- a/react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.tsx +++ b/react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.tsx @@ -1,6 +1,6 @@ import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; import { AutoMLTaskType, autoMLTaskTypeOptions, ExperimentStatus } from '@/enums'; -import { useComputingResource } from '@/hooks/useComputingResource'; +import { useSystemResource } from '@/hooks/useComputingResource'; import { classifierAlgorithms, FrameworkType, @@ -39,7 +39,7 @@ function BasicInfo({ instanceStatus, isInstance = false, }: BasicInfoProps) { - const getResourceDescription = useComputingResource()[1]; + const getResourceDescription = useSystemResource(); const basicDatas: BasicInfoData[] = useMemo(() => { if (!info) { return []; diff --git a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx index 6f7870d8..faf83b56 100644 --- a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx +++ b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx @@ -6,7 +6,7 @@ import { autoMLEnsembleClassOptions, autoMLTaskTypeOptions, } from '@/enums'; -import { useComputingResource } from '@/hooks/useComputingResource'; +import { useSystemResource } from '@/hooks/useComputingResource'; import { classificationAlgorithms, featureAlgorithms, @@ -76,7 +76,7 @@ function AutoMLBasic({ instanceStatus, isInstance = false, }: AutoMLBasicProps) { - const getResourceDescription = useComputingResource()[1]; + const getResourceDescription = useSystemResource(); const basicDatas: BasicInfoData[] = useMemo(() => { if (!info) { return []; diff --git a/react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx b/react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx index 8bb68cf9..ea8f9765 100644 --- a/react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx +++ b/react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx @@ -1,30 +1,8 @@ -import { getAccessToken } from '@/access'; -import KFIcon from '@/components/KFIcon'; import KFModal from '@/components/KFModal'; -import { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config'; +import { CategoryData, DataSource } from '@/pages/Dataset/config'; import { addDataset } from '@/services/dataset/index.js'; import { to } from '@/utils/promise'; -import { - getFileListFromEvent, - limitUploadFileType, - removeUploadedFile, - validateUploadFiles, -} from '@/utils/ui'; -import { - Button, - Form, - Input, - Radio, - Select, - Upload, - UploadFile, - message, - type ModalProps, - type UploadProps, -} from 'antd'; -import { omit } from 'lodash'; -import { useState } from 'react'; -import styles from './index.less'; +import { Form, Input, Radio, Select, message, type ModalProps } from 'antd'; interface AddDatasetModalProps extends Omit { typeList: CategoryData[]; @@ -33,20 +11,6 @@ interface AddDatasetModalProps extends Omit { } function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalProps) { - const [uuid] = useState(Date.now()); - - // 上传组件参数 - const uploadProps: UploadProps = { - action: resourceConfig[ResourceType.Dataset].uploadAction, - headers: { - Authorization: getAccessToken() || '', - }, - defaultFileList: [], - accept: '.zip,.tgz', - beforeUpload: limitUploadFileType('zip,tgz'), - onRemove: removeUploadedFile, - }; - // 上传请求 const createDataset = async (params: any) => { const [res] = await to(addDataset(params)); @@ -58,22 +22,11 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr // 提交 const onFinish = (formData: any) => { - const fileList: UploadFile[] = formData['fileList'] ?? []; - if (validateUploadFiles(fileList)) { - const params = { - ...omit(formData, ['fileList']), - dataset_source: DataSource.Create, - dataset_version_vos: fileList.map((item) => { - const data = item.response?.data?.[0] ?? {}; - return { - file_name: data.fileName, - file_size: data.fileSize, - url: data.url, - }; - }), - }; - createDataset(params); - } + const params = { + ...formData, + dataset_source: DataSource.Create, + }; + createDataset(params); }; return ( @@ -108,32 +61,6 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr > - { - if (value === 'master') { - return Promise.reject(`数据集版本不能为 master`); - } else if (value === 'origin') { - return Promise.reject(`数据集版本不能为 origin`); - } - return Promise.resolve(); - }, - }, - ]} - > - - - { - if (value === 'master') { - return Promise.reject(`模型版本不能为 master`); - } else if (value === 'origin') { - return Promise.reject(`模型版本不能为 origin`); - } - return Promise.resolve(); - }, - }, - ]} - > - - + { const { message } = App.useApp(); // 获取详情 - const getResourceDetail = useCallback(async () => { - const params = { - id: resourceId, - owner, - name, - identifier, - version, - is_public, - }; - const request = config.getInfo; - const [res] = await to(request(params)); - if (res && res.data) { - setInfo(res.data); - } - }, [config, resourceId, owner, name, identifier, version, is_public]); + const getResourceDetail = useCallback( + async (version: string | undefined) => { + const params = { + id: resourceId, + owner, + name, + identifier, + version, + is_public, + }; + const request = config.getInfo; + const [res] = await to(request(params)); + if (res && res.data) { + setInfo(res.data); + } + }, + [config, resourceId, owner, name, identifier, is_public], + ); // 获取版本列表 const getVersionList = useCallback( @@ -101,14 +105,15 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { } } else { setVersion(undefined); + getResourceDetail(undefined); } }, - [config, owner, identifier, versionParam], + [config, owner, identifier, versionParam, getResourceDetail], ); useEffect(() => { if (version) { - getResourceDetail(); + getResourceDetail(version); } }, [version, getResourceDetail]); @@ -117,7 +122,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { }, [getVersionList]); // 新建版本 - const showModal = () => { + const showAddVersionModal = () => { const { close } = openAntdModal(AddVersionModal, { resourceType: resourceType, resourceId: resourceId, @@ -291,52 +296,70 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { {info.praises_count} - - 版本号: - + + + + + ) : ( + - 删除版本 - - + {info.description ?? '暂无描述'} + + )}
- setActiveTab(key)}> -
- {activeTab === ResourceInfoTabKeys.Evolution && } -
+ {version ? ( + <> + setActiveTab(key)}> +
+ {activeTab === ResourceInfoTabKeys.Evolution && } +
+ + ) : ( + + )}
); diff --git a/react-ui/src/pages/Dataset/config.tsx b/react-ui/src/pages/Dataset/config.tsx index 0969ee35..2f43bda5 100644 --- a/react-ui/src/pages/Dataset/config.tsx +++ b/react-ui/src/pages/Dataset/config.tsx @@ -13,9 +13,11 @@ import { editModelVersion, getDatasetInfo, getDatasetList, + getDatasetNextVersionReq, getDatasetVersionList, getModelInfo, getModelList, + getModelNextVersionReq, getModelVersionList, } from '@/services/dataset/index.js'; import { limitUploadFileType } from '@/utils/ui'; @@ -42,6 +44,7 @@ type ResourceTypeInfo = { deleteVersion: (params: any) => Promise; // 删除版本 getInfo: (params: any) => Promise; // 获取详情 compareVersion: (params: any) => Promise; // 版本对比 + getNextVersion: (params: any) => Promise; // 获取下一个版本 name: string; // 名称 typeParamKey: 'data_type' | 'model_type'; // 类型参数名称,获取资源列表接口使用 tagParamKey: 'data_tag' | 'model_tag'; // 标签参数名称,获取资源列表接口使用 @@ -72,6 +75,7 @@ export const resourceConfig: Record = { deleteVersion: deleteDatasetVersion, getInfo: getDatasetInfo, compareVersion: compareDatasetVersion, + getNextVersion: getDatasetNextVersionReq, name: '数据集', typeParamKey: 'data_type', tagParamKey: 'data_tag', @@ -111,6 +115,7 @@ export const resourceConfig: Record = { deleteVersion: deleteModelVersion, getInfo: getModelInfo, compareVersion: compareModelVersion, + getNextVersion: getModelNextVersionReq, name: '模型', typeParamKey: 'model_type', tagParamKey: 'model_tag', diff --git a/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx b/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx index 7339135d..5395d44e 100644 --- a/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx +++ b/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx @@ -8,7 +8,7 @@ import { CodeConfigData } from '@/components/CodeSelectorModal'; import KFIcon from '@/components/KFIcon'; import { DevEditorStatus } from '@/enums'; import { useCacheState } from '@/hooks/useCacheState'; -import { useComputingResource } from '@/hooks/useComputingResource'; +import { useSystemResource } from '@/hooks/useComputingResource'; import { DatasetData, ModelData } from '@/pages/Dataset/config'; import { deleteEditorReq, @@ -66,7 +66,7 @@ function EditorList() { pageSize: 10, }, ); - const getResourceDescription = useComputingResource()[1]; + const getResourceDescription = useSystemResource(); // 获取编辑器列表 const getEditorList = useCallback(async () => { diff --git a/react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx b/react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx index 60f0555b..a49bf6a6 100644 --- a/react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx @@ -3,12 +3,10 @@ import KFModal from '@/components/KFModal'; import { DataSource, ResourceType, - ResourceVersionData, resourceConfig, type ResourceData, } from '@/pages/Dataset/config'; import { to } from '@/utils/promise'; -import { InfoCircleOutlined } from '@ant-design/icons'; import { Form, Input, ModalProps, Select } from 'antd'; import { pick } from 'lodash'; import { useEffect, useState } from 'react'; @@ -44,7 +42,6 @@ function ExportModelModal({ }: ExportModelModalProps) { const [form] = Form.useForm(); const [resources, setResources] = useState([]); - const [versions, setVersions] = useState([]); const config = resourceConfig[resourceType]; const layout = { @@ -77,35 +74,24 @@ function ExportModelModal({ return undefined; }; - // 版本 tooltip - const getTooltip = () => { - const id = form.getFieldValue('id'); - const resource = getSelectedResource(id); - const name = resource?.name ?? ''; - const versionNames = versions.map((item: ResourceVersionData) => item.name).join('、'); - const tooltip = - versions.length > 0 ? `${name}有以下版本:\n${versionNames}\n注意不能重复` : undefined; - return tooltip; - }; - // 处理数据集、模型选择变化 const handleResourceChange = (id: number | undefined) => { if (id) { - getRecourceVersions(id); + getRecourceNextVersion(id); } else { - setVersions([]); + form.setFieldValue('version', ''); } }; - // 获取数据集、模型版本列表 - const getRecourceVersions = async (id: number) => { + // 获取数据集、模型下一个版本 + const getRecourceNextVersion = async (id: number) => { const resource = getSelectedResource(id); if (!resource) { return; } - const [res] = await to(config.getVersions(pick(resource, ['identifier', 'owner']))); + const [res] = await to(config.getNextVersion(pick(resource, ['identifier', 'owner']))); if (res && res.data) { - setVersions(res.data); + form.setFieldValue('version', res.data); } }; @@ -184,15 +170,6 @@ function ExportModelModal({ , - } - : undefined - } rules={[ { required: true, message: `请输入${config.name}版本` }, { @@ -205,8 +182,6 @@ function ExportModelModal({ return Promise.reject(`${config.name}版本不能为 master`); } else if (value === 'origin') { return Promise.reject(`${config.name}版本不能为 origin`); - } else if (value && versions.map((item) => item.name).includes(value)) { - return Promise.reject(`${config.name}版本已存在`); } else { return Promise.resolve(); } @@ -214,7 +189,13 @@ function ExportModelModal({ }, ]} > - + { if (!info) { diff --git a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx index 5d47ce71..fae1e3be 100644 --- a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx @@ -9,7 +9,7 @@ import PageTitle from '@/components/PageTitle'; import SubAreaTitle from '@/components/SubAreaTitle'; import { ServiceRunStatus, serviceStatusOptions } from '@/enums'; import { useCacheState } from '@/hooks/useCacheState'; -import { useComputingResource } from '@/hooks/useComputingResource'; +import { useSystemResource } from '@/hooks/useComputingResource'; import { ModelData } from '@/pages/Dataset/config'; import { deleteServiceVersionReq, @@ -89,7 +89,7 @@ function ServiceInfo() { format: formatDate, }, ]; - const getResourceDescription = useComputingResource()[1]; + const getResourceDescription = useSystemResource(); // 获取服务详情 const getServiceInfo = useCallback(async () => { diff --git a/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx b/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx index 2656b946..47d03d61 100644 --- a/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx @@ -1,6 +1,6 @@ import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; import { ServiceRunStatus } from '@/enums'; -import { useComputingResource } from '@/hooks/useComputingResource'; +import { useSystemResource } from '@/hooks/useComputingResource'; import { ServiceVersionData } from '@/pages/ModelDeployment/types'; import { formatDate } from '@/utils/date'; import { formatMirror, formatModel } from '@/utils/format'; @@ -36,7 +36,7 @@ const formatEnvText = (env?: Record) => { }; function VersionBasicInfo({ info }: BasicInfoProps) { - const getResourceDescription = useComputingResource()[1]; + const getResourceDescription = useSystemResource(); const datas: BasicInfoData[] = [ { diff --git a/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx index ee92edb2..c31e4700 100644 --- a/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx +++ b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx @@ -1,6 +1,6 @@ import KFModal from '@/components/KFModal'; import { ServiceRunStatus } from '@/enums'; -import { useComputingResource } from '@/hooks/useComputingResource'; +import { useSystemResource } from '@/hooks/useComputingResource'; import { type ServiceVersionData } from '@/pages/ModelDeployment/types'; import { getServiceVersionCompareReq } from '@/services/modelDeployment'; import { isEmpty } from '@/utils'; @@ -42,7 +42,7 @@ const formatEnvText = (env: Record) => { function VersionCompareModal({ version1, version2, ...rest }: VersionCompareModalProps) { const [compareData, setCompareData] = useState(undefined); - const getResourceDescription = useComputingResource()[1]; + const getResourceDescription = useSystemResource(); const fields: FiledType[] = useMemo( () => [ diff --git a/react-ui/src/pages/Pipeline/Info/utils.tsx b/react-ui/src/pages/Pipeline/Info/utils.tsx index eccad30c..2feb70c6 100644 --- a/react-ui/src/pages/Pipeline/Info/utils.tsx +++ b/react-ui/src/pages/Pipeline/Info/utils.tsx @@ -70,15 +70,6 @@ export function createMenuItems( } } -export function getInParameterComponent( - parameter: PipelineNodeModelParameter, -): React.ReactNode | null { - if (parameter.value) { - } - - return null; -} - // 判断是否允许输入 export function canInput(parameter: PipelineNodeModelParameter) { const { type, item_type } = parameter; @@ -87,6 +78,9 @@ export function canInput(parameter: PipelineNodeModelParameter) { (item_type === 'dataset' || item_type === 'model' || item_type === 'image' || - item_type === 'code') + item_type === 'code' || + item_type === 'remote-dataset' || + item_type === 'remote-model' || + item_type === 'remote-code') ); } diff --git a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx index 32bbbc72..0ceee5d5 100644 --- a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx +++ b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx @@ -1,7 +1,10 @@ import CodeSelectorModal, { CodeConfigData } from '@/components/CodeSelectorModal'; import KFIcon from '@/components/KFIcon'; import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; -import ParameterSelect, { type ParameterSelectDataType } from '@/components/ParameterSelect'; +import ParameterSelect, { + type ParameterSelectDataType, + type ParameterSelectObject, +} from '@/components/ParameterSelect'; import ResourceSelectorModal, { ResourceSelectorType, selectorTypeConfig, @@ -9,6 +12,7 @@ import ResourceSelectorModal, { import SubAreaTitle from '@/components/SubAreaTitle'; import { CommonTabKeys, ComponentType } from '@/enums'; import { canInput, createMenuItems } from '@/pages/Pipeline/Info/utils'; +import state from '@/state/jcdResource'; import { PipelineGlobalParam, PipelineNodeModel, @@ -20,6 +24,7 @@ import { to } from '@/utils/promise'; import { removeFormListItem } from '@/utils/ui'; import { MinusCircleOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { INode } from '@antv/g6'; +import { useSnapshot } from '@umijs/max'; import { Button, Drawer, Flex, Form, Input, MenuProps } from 'antd'; import { RuleObject } from 'antd/es/form'; import { NamePath } from 'antd/es/form/interface'; @@ -38,6 +43,17 @@ type PipelineNodeParameterProps = { onFormChange: (data: PipelineNodeModelSerialize) => void; }; +// 自定义的下拉组件类型 +const parameterSelectList = [ + 'dataset', + 'model', + 'service', + 'resource', + 'remote-resource-type', + 'remote-image', + 'remote-resource', +]; + const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParameterProps, ref) => { const [form] = Form.useForm(); const [stagingItem, setStagingItem] = useState( @@ -45,6 +61,8 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete ); const [open, setOpen] = useState(false); const [menuItems, setMenuItems] = useState([]); + const snap = useSnapshot(state); + const { setCurrentType } = snap; const afterOpenChange = async () => { if (!open) { @@ -119,6 +137,12 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete // 参数下拉菜单 setMenuItems(createMenuItems(params, parentNodes)); + + // 云际组件,设置 store 当前资源类型 + if (model.id.startsWith('remote-task')) { + const resourceType = model.in_parameters['--resource_type'].value; + setCurrentType(resourceType); + } }, close: () => { onClose(); @@ -136,7 +160,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete } }, }), - [form, open], + [form, open, setCurrentType], ); // ref 类型选择 @@ -144,7 +168,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete formItemName: NamePath, item: PipelineNodeModelParameter | Pick, ) => { - if (item.item_type === 'code') { + if (item.item_type === 'code' || item.item_type === 'remote-code') { selectCodeConfig(formItemName, item); } else { selectResource(formItemName, item); @@ -183,9 +207,11 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete let type: ResourceSelectorType; switch (item.item_type) { case 'dataset': + case 'remote-dataset': type = ResourceSelectorType.Dataset; break; case 'model': + case 'remote-model': type = ResourceSelectorType.Model; break; default: @@ -249,14 +275,14 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete // 获取选择数据集、模型后面按钮 icon const getSelectBtnIcon = (item: { item_type: string }) => { const type = item.item_type; - if (type === 'code') { + if (type === 'code' || type === 'remote-code') { return ; } let selectorType: ResourceSelectorType; - if (type === 'dataset') { + if (type === 'dataset' || type === 'remote-dataset') { selectorType = ResourceSelectorType.Dataset; - } else if (type === 'model') { + } else if (type === 'model' || type === 'remote-model') { selectorType = ResourceSelectorType.Model; } else { selectorType = ResourceSelectorType.Mirror; @@ -331,6 +357,21 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete return rules; }; + // 云际组件,选择类型后,重置镜像和资源,获取镜像、资源列表 + const handleParameterSelect = ( + value: ParameterSelectObject, + itemType: string, + parentName: string, + ) => { + if (itemType === 'remote-resource-type') { + snap.setCurrentType(value.value); + const remoteImage = form.getFieldValue([parentName, '--image']); + form.setFieldValue([parentName, '--image'], { ...remoteImage, value: undefined }); + const remoteResource = form.getFieldValue([parentName, '--resource']); + form.setFieldValue([parentName, '--resource'], { ...remoteResource, value: undefined }); + } + }; + // 表单组件 const getFormComponent = ( item: { key: string; value: PipelineNodeModelParameter }, @@ -361,12 +402,18 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete )} {item.value.type === ComponentType.Select && - (['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? ( + (parameterSelectList.includes(item.value.item_type) ? ( + handleParameterSelect( + value as ParameterSelectObject, + item.value.item_type, + parentName, + ) + } /> ) : null)} diff --git a/react-ui/src/services/dataset/index.js b/react-ui/src/services/dataset/index.js index c67908b7..7d5571dc 100644 --- a/react-ui/src/services/dataset/index.js +++ b/react-ui/src/services/dataset/index.js @@ -90,6 +90,15 @@ export function compareDatasetVersion(data) { }); } +// 获取数据集下个版本号 +export function getDatasetNextVersionReq(data) { + return request(`/api/mmp/newdataset/queryNextVersion`, { + method: 'POST', + data, + }); +} + + // ----------------------------模型--------------------------------- // 分页查询模型列表 @@ -157,6 +166,14 @@ export function deleteModelVersion(params) { }); } +// 获取模型下个版本号 +export function getModelNextVersionReq(data) { + return request(`/api/mmp/newmodel/queryNextVersion`, { + method: 'POST', + data, + }); +} + // 获取模型依赖 export function getModelAtlasReq(params) { return request(`/api/mmp/newmodel/getModelDependencyTree`, { @@ -218,4 +235,5 @@ export function unpraiseResourceReq(id) { return request(`/api/mmp/newmodel/unpraise/${id}`, { method: 'DELETE', }); -} \ No newline at end of file +} + diff --git a/react-ui/src/services/external/index.ts b/react-ui/src/services/external/index.ts new file mode 100644 index 00000000..22c7b439 --- /dev/null +++ b/react-ui/src/services/external/index.ts @@ -0,0 +1,87 @@ +// 外部系统 + +import { request } from '@umijs/max'; + +// 云际系统登录 +export function jccLoginReq() { + return request(`http://119.45.255.234:30180/jcc-admin/admin/login`, { + method: 'POST', + data: { + username: 'iflytek', + password: 'iflytek@123', + }, + headers: { + isToken: false, + }, + skipLoading: true, + skipValidating: true, + }); +} + +// 云际系统获取资源类型 +export function jccGetResourceTypesReq(token: string, userId: number) { + return request(`http://119.45.255.234:30180/jsm/jobSet/resourceRange`, { + method: 'POST', + data: { + userID: userId, + }, + headers: { + authorization: `${token}`, + isToken: false, + }, + skipLoading: true, + skipValidating: true, + }); +} + +// 云际系统获取资源镜像 +export function jccGetImagesReq(token: string, cardTypes: string[]) { + return request(`http://119.45.255.234:30180/jsm/jobSet/queryImages`, { + method: 'POST', + data: { + cardTypes: cardTypes, + }, + headers: { + authorization: `${token}`, + isToken: false, + }, + skipLoading: true, + skipValidating: true, + }); +} + +// 云际系统获取资源列表 +export function jccGetResourcesReq(token: string, cardType: string) { + return request(`http://119.45.255.234:30180/jsm/jobSet/queryResource`, { + method: 'POST', + data: { + queryResource: { + cpu: { + min: 0, + max: 0, + }, + memory: { + min: 0, + max: 0, + }, + gpu: { + min: 0, + max: 0, + }, + storage: { + min: 0, + max: 0, + }, + type: cardType, + }, + resourceType: 'Train', + clusterIDs: ['1865927992266461184', ''], + }, + headers: { + authorization: `${token}`, + isToken: false, + }, + skipLoading: true, + skipValidating: true, + }); +} diff --git a/react-ui/src/state/jcdResource.ts b/react-ui/src/state/jcdResource.ts new file mode 100644 index 00000000..69fe7b91 --- /dev/null +++ b/react-ui/src/state/jcdResource.ts @@ -0,0 +1,140 @@ +import { + jccGetImagesReq, + jccGetResourcesReq, + jccGetResourceTypesReq, + jccLoginReq, +} from '@/services/external'; +import { to } from '@/utils/promise'; +import { proxy } from '@umijs/max'; + +export type JCCResourceRange = { + type: string; +}; + +export type JCCResourceType = { + label: string; + value: string; +}; + +export interface JCCResourceImage { + imageID: number; + name: string; + createTime: Date; + clusterImages: JCCClusterImage[]; +} + +export interface JCCClusterImage { + imageID: number; + clusterID: string; + originImageType: string; + originImageID: string; + originImageName: string; + cards: JCCCard[]; +} + +export interface JCCCard { + originImageID: string; + card: string; +} + +export interface JCCResourceStandard { + id: number; + sourceKey: string; + type: string; + name: string; + totalCount: number; + availableCount: number; + changeType: number; + status: number; + region: string; + clusterId: string; + costPerUnit: number; + costType: string; + tag: string; + userId: number; + createTime: Date; + updateTime: Date; + baseResourceSpecs: JCCBaseResourceSpec[]; +} + +export interface JCCBaseResourceSpec { + id: number; + resourceSpecId: number; + type: string; + name: string; + totalValue: number; + totalUnit: string; + availableValue: number; + availableUnit: string; + userId: number; + createTime: Date; + updateTime: Date; +} + +type JCCResourceTypeStore = { + token: string; + types: JCCResourceType[]; + images: JCCResourceImage[]; + resources: JCCResourceStandard[]; + currentType: string | undefined | null; + jccLogin: () => void; + getResourceTypes: () => void; + getImages: (cardTypes: string[]) => void; + getResources: (cardType: string) => void; + setCurrentType: (cardType: string) => void; +}; + +const state = proxy({ + token: '', + types: [], + images: [], + resources: [], + currentType: undefined, + jccLogin: async () => { + const [res] = await to(jccLoginReq()); + if (res && res.code === 200 && res.data) { + const { tokenHead, token } = res.data; + state.token = tokenHead + token; + } + }, + getResourceTypes: async () => { + const [loginRes] = await to(jccLoginReq()); + if (loginRes && loginRes.code === 200 && loginRes.data) { + const { tokenHead, token, jsmUserInfo } = loginRes.data; + state.token = tokenHead + token; + const userID = jsmUserInfo?.data?.userID; + const [res] = await to(jccGetResourceTypesReq(tokenHead + token, userID)); + if (res && res.code === 'OK' && res.data) { + state.types = res.data.resourceRanges?.map((v: JCCResourceRange) => ({ + label: v.type, + value: v.type, + })); + } + } + }, + getImages: async (cardTypes: string[]) => { + const [res] = await to(jccGetImagesReq(state.token, cardTypes)); + if (res && res.code === 'OK' && res.data) { + state.images = res.data.images; + } + }, + getResources: async (cardType: string) => { + const [res] = await to(jccGetResourcesReq(state.token, cardType)); + if (res && res.code === 'OK' && res.data) { + state.resources = res.data.resource; + } + }, + setCurrentType: (cardType: string | undefined | null) => { + if (state.currentType !== cardType) { + state.currentType = cardType; + state.images = []; + state.resources = []; + if (cardType) { + state.getImages([cardType]); + state.getResources(cardType); + } + } + }, +}); + +export default state; diff --git a/react-ui/src/state/systemResource.ts b/react-ui/src/state/systemResource.ts new file mode 100644 index 00000000..0d7d6b15 --- /dev/null +++ b/react-ui/src/state/systemResource.ts @@ -0,0 +1,53 @@ +/* + * @Author: 赵伟 + * @Date: 2024-10-10 08:51:41 + * @Description: 资源规格 + */ + +import { getComputingResourceReq } from '@/services/pipeline'; +import { ComputingResource } from '@/types'; +import { to } from '@/utils/promise'; +import { proxy } from '@umijs/max'; +import { type SelectProps } from 'antd'; + +/** 过滤资源规格 */ +export const filterResourceStandard: SelectProps['filterOption'] = ( + input: string, + option?: ComputingResource, +) => { + return ( + option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? false + ); +}; + +/** 资源规格字段 */ +export const resourceFieldNames = { + label: 'description', + value: 'id', +}; + +export interface SystemResourceStore { + resources: ComputingResource[]; +} + +const state = proxy({ + resources: [], +}); + +/** 获取资源列表 */ +export const getSystemResources = async () => { + if (state.resources.length > 0) { + return; + } + const params = { + page: 0, + size: 1000, + resource_type: '', + }; + const [res] = await to(getComputingResourceReq(params)); + if (res && res.data && Array.isArray(res.data.content)) { + state.resources = res.data.content; + } +}; + +export default state; diff --git a/react-ui/src/utils/sessionStorage.ts b/react-ui/src/utils/sessionStorage.ts index 88e48f79..0bbdd1f2 100644 --- a/react-ui/src/utils/sessionStorage.ts +++ b/react-ui/src/utils/sessionStorage.ts @@ -13,6 +13,8 @@ export default class SessionStorage { static readonly aimUrlKey = 'aim-url'; /** tensorBoard url */ static readonly tensorBoardUrlKey = 'tensor-board-url'; + // /** 云际系统 Token */ + // static readonly jccTokenKey = 'jcc-token'; /** * 获取 SessionStorage 值