From 69ec89959c27e5ef2abe25ef7e66a3ae9c6869a3 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Tue, 18 Jun 2024 08:44:15 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E5=AE=9E=E9=AA=8C=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E5=AF=BC=E5=87=BA=E5=88=B0=E6=A8=A1=E5=9E=8B=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/AddExperimentModal/index.less | 2 +- .../components/AddExperimentModal/index.tsx | 2 +- .../components/ExperimentResult/index.less | 1 - .../components/ExperimentResult/index.tsx | 35 ++- .../components/ExportModelModal/index.less | 7 + .../components/ExportModelModal/index.tsx | 207 ++++++++++++++++++ react-ui/src/services/dataset/index.js | 9 +- 7 files changed, 255 insertions(+), 8 deletions(-) create mode 100644 react-ui/src/pages/Experiment/components/ExportModelModal/index.less create mode 100644 react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx diff --git a/react-ui/src/pages/Experiment/components/AddExperimentModal/index.less b/react-ui/src/pages/Experiment/components/AddExperimentModal/index.less index eec152a7..2470e868 100644 --- a/react-ui/src/pages/Experiment/components/AddExperimentModal/index.less +++ b/react-ui/src/pages/Experiment/components/AddExperimentModal/index.less @@ -1,4 +1,4 @@ -.modal { +.add-experiment-modal { .global_param_item { max-height: 230px; padding: 24px 12px 0; diff --git a/react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx b/react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx index 71ec2f06..becfc0a7 100644 --- a/react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx +++ b/react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx @@ -115,7 +115,7 @@ function AddExperimentModal({ }; return ( { - downLoadZip(`/api/mmp/minioStorage/download`, { path: val }); + const { message } = App.useApp(); + + // 下载 + const download = (path: string) => { + downLoadZip(`/api/mmp/minioStorage/download`, { path }); + }; + + // 导出到模型库 + const exportToModel = (path: string) => { + const { close } = openAntdModal(ExportModelModal, { + path, + onOk: () => { + message.success('导出成功'); + close(); + }, + }); }; return ( @@ -31,12 +48,22 @@ function ExperimentResult({ results }: ExperimentResultProps) { + {/* 导出到模型库 导出到数据集 */} diff --git a/react-ui/src/pages/Experiment/components/ExportModelModal/index.less b/react-ui/src/pages/Experiment/components/ExportModelModal/index.less new file mode 100644 index 00000000..250f56a3 --- /dev/null +++ b/react-ui/src/pages/Experiment/components/ExportModelModal/index.less @@ -0,0 +1,7 @@ +.export-model-modal__tooltip { + :global { + .ant-tooltip-inner { + white-space: pre-line; + } + } +} diff --git a/react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx b/react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx new file mode 100644 index 00000000..b1d09da0 --- /dev/null +++ b/react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx @@ -0,0 +1,207 @@ +import editExperimentIcon from '@/assets/img/edit-experiment.png'; +import KFModal from '@/components/KFModal'; +import { type ResourceData } from '@/pages/Dataset/config'; +import { + addModelsVersionDetail, + exportModelReq, + getModelList, + getModelVersionsById, +} from '@/services/dataset'; +import { to } from '@/utils/promise'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { Form, Input, ModalProps, Select } from 'antd'; +import { useEffect, useState } from 'react'; +import styles from './index.less'; + +type FormData = { + models_id: string; + version: string; + description: string; +}; + +type ExportModelResponce = { + fileName: string; + fileSize: string; + url: string; +}; + +type CreateModelVersionParams = FormData & { + file_name: string; + file_size: string; + url: string; + // name: string; +}; + +interface ExportModelModalProps extends Omit { + path: string; + onOk: () => void; +} + +function ExportModelModal({ path, onOk, ...rest }: ExportModelModalProps) { + const [form] = Form.useForm(); + const [models, setModels] = useState([]); + const [versions, setVersions] = useState([]); + const [uuid] = useState(Date.now()); + + const layout = { + labelCol: { span: 24 }, + wrapperCol: { span: 24 }, + }; + + useEffect(() => { + requestModelList(); + }, []); + + // 模型版本tooltip + const getTooltip = () => { + const id = form.getFieldValue('models_id'); + const name = models.find((item) => item.id === id)?.name ?? ''; + const tooltip = + versions.length > 0 ? `${name}有以下版本:\n${versions.join('、')}\n注意不能重复` : undefined; + return tooltip; + }; + + // 处理模型名称变化 + const handleModelChange = (id: number | undefined) => { + if (id) { + getModelVersions(id); + } else { + setVersions([]); + } + }; + + // 获取模型列表 + const requestModelList = async () => { + const params = { + page: 0, + size: 1000, + available_range: 0, // 个人 + }; + const [res] = await to(getModelList(params)); + if (res && res.data) { + setModels(res.data.content || []); + } + }; + + // 获取模型版本列表 + const getModelVersions = async (id: number) => { + const [res] = await to(getModelVersionsById(id)); + if (res && res.data) { + setVersions(res.data); + } + }; + + // 提交 + const hanldeFinish = (formData: FormData) => { + exportToModel(formData); + }; + + // 导出到模型 + const exportToModel = async (formData: FormData) => { + const params = { + uuid: String(uuid), + path, + }; + const [res] = await to(exportModelReq(params)); + if (res && res.data) { + const files = res.data as ExportModelResponce[]; + const params: CreateModelVersionParams[] = files.map((item) => ({ + ...formData, + file_name: item.fileName, + file_size: item.fileSize, + url: item.url, + })); + + createModelVersion(params); + } + }; + + // 创建模型版本 + const createModelVersion = async (params: CreateModelVersionParams[]) => { + const [res] = await to(addModelsVersionDetail(params)); + if (res) { + onOk(); + } + }; + + return ( + +
+ + + + , + } + : undefined + } + rules={[ + { required: true, message: '请输入模型版本' }, + { + validator: (_, value) => { + if (value && versions.includes(value)) { + return Promise.reject('模型版本已存在'); + } else { + return Promise.resolve(); + } + }, + }, + ]} + > + + + + + +
+
+ ); +} + +export default ExportModelModal; diff --git a/react-ui/src/services/dataset/index.js b/react-ui/src/services/dataset/index.js index e59d7a21..1764c8d0 100644 --- a/react-ui/src/services/dataset/index.js +++ b/react-ui/src/services/dataset/index.js @@ -130,7 +130,6 @@ export function deleteDataset(id) { method: 'DELETE', }); } - // 获取模型依赖 export function getModelAtlasReq(data) { return request(`/api/mmp/modelDependency/queryModelAtlas`, { @@ -138,3 +137,11 @@ export function getModelAtlasReq(data) { data }); } + +// 实验结果导出到模型 +export function exportModelReq(data) { + return request(`/api/mmp/models/exportModel`, { + method: 'POST', + data + }); +} \ No newline at end of file From 0753030d9b6a8722079ed965b56d8d95a7987b7b Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Tue, 18 Jun 2024 09:28:14 +0800 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20=E6=A8=A1=E5=9E=8B=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E8=B7=B3=E8=BD=AC=E5=AE=9E=E9=AA=8C=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/pages/Model/components/ModelEvolution/utils.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx b/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx index 3c3f4225..dfac7831 100644 --- a/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx +++ b/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx @@ -64,6 +64,7 @@ export type ModalDetail = { export interface ModelDepsAPIData { current_model_id: number; version: string; + workflow_id: number; exp_ins_id: number; model_type: NodeType.children | NodeType.current | NodeType.parent; current_model_name: string; From 0bbfa01286d80a271c82cb204fb51dccf3feb1d8 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Tue, 18 Jun 2024 10:47:37 +0800 Subject: [PATCH 3/7] =?UTF-8?q?chore:=20=E6=A8=A1=E5=9E=8B=E6=BC=94?= =?UTF-8?q?=E5=8C=96=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ResourceIntro/index.tsx | 2 +- .../Model/components/ModelEvolution/index.tsx | 45 +------ .../Model/components/ModelEvolution/utils.tsx | 4 +- .../Model/components/NodeTooltips/index.tsx | 123 ++++++++++++++---- 4 files changed, 110 insertions(+), 64 deletions(-) diff --git a/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx b/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx index 91a63582..4e1b8c4d 100644 --- a/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx @@ -33,7 +33,7 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { useEffect(() => { getModelByDetail(); getVersionList(); - }, []); + }, [resourceId]); // 获取详情 const getModelByDetail = async () => { diff --git a/react-ui/src/pages/Model/components/ModelEvolution/index.tsx b/react-ui/src/pages/Model/components/ModelEvolution/index.tsx index 6a6e74e8..f277b634 100644 --- a/react-ui/src/pages/Model/components/ModelEvolution/index.tsx +++ b/react-ui/src/pages/Model/components/ModelEvolution/index.tsx @@ -5,21 +5,13 @@ import themes from '@/styles/theme.less'; import { to } from '@/utils/promise'; import G6, { G6GraphEvent, Graph } from '@antv/g6'; // @ts-ignore -import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceIntro'; import { Flex, Select } from 'antd'; import { useEffect, useRef, useState } from 'react'; import GraphLegend from '../GraphLegend'; import NodeTooltips from '../NodeTooltips'; import styles from './index.less'; import type { ModelDepsData, ProjectDependency, TrainDataset } from './utils'; -import { - NodeType, - getGraphData, - nodeFontSize, - nodeHeight, - nodeWidth, - normalizeTreeData, -} from './utils'; +import { getGraphData, nodeFontSize, nodeHeight, nodeWidth, normalizeTreeData } from './utils'; type modeModelEvolutionProps = { resourceId: number; @@ -80,7 +72,7 @@ function ModelEvolution({ width: graphRef.current!.clientWidth, height: graphRef.current!.clientHeight, fitView: true, - fitViewPadding: [100, 100, 100, 100], + fitViewPadding: 200, minZoom: 0.5, maxZoom: 5, defaultNode: { @@ -172,36 +164,7 @@ function ModelEvolution({ const nodeItem = e.item; const model = nodeItem.getModel() as ModelDepsData | ProjectDependency | TrainDataset; const { model_type } = model; - const { origin } = location; - let url: string = ''; switch (model_type) { - case NodeType.children: - case NodeType.parent: { - const { current_model_id, version } = model; - url = `${origin}/dataset/model/${current_model_id}?tab=${ResourceInfoTabKeys.Evolution}&version=${version}`; - break; - } - case NodeType.project: { - const { url: projectUrl } = model; - url = projectUrl; - break; - } - case NodeType.trainDataset: - case NodeType.testDataset: { - const { dataset_id, dataset_version } = model; - url = `${origin}/dataset/dataset/${dataset_id}?tab=${ResourceInfoTabKeys.Version}&version=${dataset_version}`; - break; - } - case NodeType.current: { - // TODO: 隐藏数据集和项目 - break; - } - default: - break; - } - - if (url) { - window.open(url, '_blank'); } }); @@ -234,6 +197,8 @@ function ModelEvolution({ graph.data(graphData); graph.render(); graph.fitView(); + setShowNodeTooltip(false); + setEnterTooltip(false); } else { clearGraphData(); } @@ -266,9 +231,11 @@ function ModelEvolution({
{(showNodeTooltip || enterTooltip) && ( diff --git a/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx b/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx index dfac7831..a878321a 100644 --- a/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx +++ b/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx @@ -3,8 +3,8 @@ import { EdgeConfig, GraphData, LayoutConfig, NodeConfig, TreeGraphData, Util } // @ts-ignore import Hierarchy from '@antv/hierarchy'; -export const nodeWidth = 110; -export const nodeHeight = 50; +export const nodeWidth = 90; +export const nodeHeight = 40; export const vGap = nodeHeight + 20; export const hGap = nodeWidth; export const ellipseWidth = nodeWidth; diff --git a/react-ui/src/pages/Model/components/NodeTooltips/index.tsx b/react-ui/src/pages/Model/components/NodeTooltips/index.tsx index a4b3f13b..217222da 100644 --- a/react-ui/src/pages/Model/components/NodeTooltips/index.tsx +++ b/react-ui/src/pages/Model/components/NodeTooltips/index.tsx @@ -1,20 +1,35 @@ +import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceIntro'; import { formatDate } from '@/utils/date'; +import { useNavigate } from '@umijs/max'; import { ModelDepsData, NodeType, ProjectDependency, TrainDataset } from '../ModelEvolution/utils'; import styles from './index.less'; -type NodeTooltipsProps = { - data?: ModelDepsData | ProjectDependency | TrainDataset; - x: number; - y: number; - onMouseEnter?: () => void; - onMouseLeave?: () => void; +type ModelInfoProps = { + resourceId: number; + data: ModelDepsData; + onVersionChange: (version: string) => void; }; -function ModelInfo({ data }: { data: ModelDepsData }) { +function ModelInfo({ resourceId, data, onVersionChange }: ModelInfoProps) { + const navigate = useNavigate(); + const gotoExperimentPage = () => { if (data.train_task?.ins_id) { const { origin } = location; - window.open(`${origin}/pipeline/experiment/144/${data.train_task.ins_id}`, '_blank'); + const url = `${origin}/pipeline/experiment/${data.workflow_id}/${data.train_task.ins_id}`; + window.open(url, '_blank'); + } + }; + + const gotoModelPage = () => { + if (data.model_type === NodeType.current) { + return; + } + if (data.current_model_id === resourceId) { + onVersionChange?.(data.version); + } else { + const path = `/dataset/model/${data.current_model_id}?tab=${ResourceInfoTabKeys.Evolution}&version=${data.version}`; + navigate(path); } }; @@ -24,9 +39,18 @@ function ModelInfo({ data }: { data: ModelDepsData }) {
模型名称: - - {data.model_version_dependcy_vo.name || '--'} - + {data.model_type === NodeType.current ? ( + + {data.model_version_dependcy_vo?.name || '--'} + + ) : ( + + )}
模型版本: @@ -61,13 +85,12 @@ function ModelInfo({ data }: { data: ModelDepsData }) {
训练任务: - {data.train_task?.name ? ( - - {data.train_task?.name} - - ) : ( - '--' - )} +
@@ -75,13 +98,24 @@ function ModelInfo({ data }: { data: ModelDepsData }) { } function DatasetInfo({ data }: { data: TrainDataset }) { + const gotoDatasetPage = () => { + const { origin } = location; + const url = `${origin}/dataset/dataset/${data.dataset_id}?tab=${ResourceInfoTabKeys.Version}&version=${data.dataset_version}`; + window.open(url, '_blank'); + }; + return ( <>
数据集信息
数据集名称: - {data.dataset_name || '--'} +
数据集版本: @@ -95,13 +129,23 @@ function DatasetInfo({ data }: { data: TrainDataset }) { } function ProjectInfo({ data }: { data: ProjectDependency }) { + const gotoProjectPage = () => { + const { url } = data; + window.open(url, '_blank'); + }; + return ( <>
项目信息
项目名称: - {data.name || '--'} +
项目分支: @@ -116,7 +160,42 @@ function ProjectInfo({ data }: { data: ProjectDependency }) { ); } -function NodeTooltips({ data, x, y, onMouseEnter, onMouseLeave }: NodeTooltipsProps) { +type ValueLinkProps = { + value: string | undefined; + onClick?: () => void; + className?: string; + nullClassName?: string; +}; + +const ValueLink = ({ value, onClick, className, nullClassName }: ValueLinkProps) => { + return value ? ( + + {value} + + ) : ( + -- + ); +}; + +type NodeTooltipsProps = { + resourceId: number; + data: ModelDepsData | ProjectDependency | TrainDataset; + x: number; + y: number; + onMouseEnter?: () => void; + onMouseLeave?: () => void; + onVersionChange: (version: string) => void; +}; + +function NodeTooltips({ + resourceId, + data, + x, + y, + onMouseEnter, + onMouseLeave, + onVersionChange, +}: NodeTooltipsProps) { if (!data) return null; let Component = null; const { model_type } = data; @@ -129,7 +208,7 @@ function NodeTooltips({ data, x, y, onMouseEnter, onMouseLeave }: NodeTooltipsPr model_type === NodeType.parent || model_type === NodeType.current ) { - Component = ; + Component = ; } return (
Date: Tue, 18 Jun 2024 14:32:21 +0800 Subject: [PATCH 4/7] =?UTF-8?q?chore:=20=E4=BC=98=E5=8C=96=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E9=9B=86=E5=92=8C=E6=A8=A1=E5=9E=8B=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/AddVersionModal/index.tsx | 11 ++++---- .../Dataset/components/CategoryItem/index.tsx | 5 ++-- .../Dataset/components/CategoryList/index.tsx | 7 +++--- .../components/ResourceIntro/index.tsx | 13 +++++----- .../Dataset/components/ResourceList/index.tsx | 15 +++++------ .../Dataset/components/ResourcePage/index.tsx | 17 +++---------- .../components/ResourceVersion/index.tsx | 13 +++++----- .../ResourceSelectorModal/index.tsx | 25 +++++++++---------- 8 files changed, 49 insertions(+), 57 deletions(-) diff --git a/react-ui/src/pages/Dataset/components/AddVersionModal/index.tsx b/react-ui/src/pages/Dataset/components/AddVersionModal/index.tsx index 9b20147a..5cb8b9a7 100644 --- a/react-ui/src/pages/Dataset/components/AddVersionModal/index.tsx +++ b/react-ui/src/pages/Dataset/components/AddVersionModal/index.tsx @@ -33,10 +33,11 @@ function AddVersionModal({ ...rest }: AddVersionModalProps) { const [uuid] = useState(Date.now()); + const config = resourceConfig[resourceType]; // 上传组件参数 const uploadProps: UploadProps = { - action: resourceConfig[resourceType].uploadAction, + action: config.uploadAction, headers: { Authorization: getAccessToken() || '', }, @@ -45,7 +46,7 @@ function AddVersionModal({ // 上传请求 const createDatasetVersion = async (params: any) => { - const request = resourceConfig[resourceType].addVersion; + const request = config.addVersion; const [res] = await to(request(params)); if (res) { message.success('创建成功'); @@ -62,7 +63,7 @@ function AddVersionModal({ const data = item.response?.data?.[0] ?? {}; return { ...otherParams, - [resourceConfig[resourceType].idParamKey]: resourceId, + [config.idParamKey]: resourceId, file_name: data.fileName, file_size: data.fileSize, url: data.url, @@ -72,8 +73,8 @@ function AddVersionModal({ } }; - const name = resourceConfig[resourceType].name; - const accept = resourceConfig[resourceType].uploadAccept; + const name = config.name; + const accept = config.uploadAccept; return ( {item.name} diff --git a/react-ui/src/pages/Dataset/components/CategoryList/index.tsx b/react-ui/src/pages/Dataset/components/CategoryList/index.tsx index 87980da7..8582168e 100644 --- a/react-ui/src/pages/Dataset/components/CategoryList/index.tsx +++ b/react-ui/src/pages/Dataset/components/CategoryList/index.tsx @@ -29,15 +29,14 @@ function CategoryList({ onTagSelect, onSearch, }: CategoryProps) { + const config = resourceConfig[resourceType]; return (
-
- {resourceConfig[resourceType].typeTitle} -
+
{config.typeTitle}
{typeList?.map((item) => (
- {resourceConfig[resourceType].tagTitle} + {config.tagTitle}
{tagList?.map((item) => ( diff --git a/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx b/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx index 4e1b8c4d..bcbd123f 100644 --- a/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx @@ -28,7 +28,8 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { const [version, setVersion] = useState(undefined); const [activeTab, setActiveTab] = useState(defaultTab); const resourceId = Number(locationParams.id); - const typeName = resourceConfig[resourceType].name; // 数据集/模型 + const config = resourceConfig[resourceType]; + const typeName = config.name; // 数据集/模型 useEffect(() => { getModelByDetail(); @@ -37,7 +38,7 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { // 获取详情 const getModelByDetail = async () => { - const request = resourceConfig[resourceType].getInfo; + const request = config.getInfo; const [res] = await to(request(resourceId)); if (res) { setInfo(res.data); @@ -46,7 +47,7 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { // 获取版本列表 const getVersionList = async () => { - const request = resourceConfig[resourceType].getVersions; + const request = config.getVersions; const [res] = await to(request(resourceId)); if (res && res.data && res.data.length > 0) { setVersionList( @@ -122,10 +123,8 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { }); } - const infoTypePropertyName = resourceConfig[resourceType] - .infoTypePropertyName as keyof ResourceData; - const infoTagPropertyName = resourceConfig[resourceType] - .infoTagPropertyName as keyof ResourceData; + const infoTypePropertyName = config.infoTypePropertyName as keyof ResourceData; + const infoTagPropertyName = config.infoTagPropertyName as keyof ResourceData; return (
diff --git a/react-ui/src/pages/Dataset/components/ResourceList/index.tsx b/react-ui/src/pages/Dataset/components/ResourceList/index.tsx index 830a9e1c..e7891141 100644 --- a/react-ui/src/pages/Dataset/components/ResourceList/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceList/index.tsx @@ -54,6 +54,7 @@ function ResourceList( const [searchText, setSearchText] = useState(initialSearchText); const [inputText, setInputText] = useState(initialSearchText); const { message } = App.useApp(); + const config = resourceConfig[resourceType]; useEffect(() => { getDataList(); @@ -82,12 +83,12 @@ function ResourceList( const params = { page: pagination.current! - 1, size: pagination.pageSize, - [resourceConfig[resourceType].typeParamKey]: dataType, - [resourceConfig[resourceType].tagParamKey]: dataTag, + [config.typeParamKey]: dataType, + [config.tagParamKey]: dataTag, available_range: isPublic ? 1 : 0, name: searchText !== '' ? searchText : undefined, }; - const request = resourceConfig[resourceType].getList; + const request = config.getList; const [res] = await to(request(params)); if (res && res.data && res.data.content) { setDataList(res.data.content); @@ -97,7 +98,7 @@ function ResourceList( // 删除请求 const deleteRecord = async (id: number) => { - const request = resourceConfig[resourceType].deleteRecord; + const request = config.deleteRecord; const [res] = await to(request(id)); if (res) { getDataList(); @@ -113,7 +114,7 @@ function ResourceList( // 删除 const handleRemove = (record: ResourceData) => { modalConfirm({ - title: resourceConfig[resourceType].deleteModalTitle, + title: config.deleteModalTitle, onOk: () => { deleteRecord(record.id); }, @@ -129,7 +130,7 @@ function ResourceList( activeType: dataType, activeTag: dataTag, }); - const prefix = resourceConfig[resourceType].prefix; + const prefix = config.prefix; navigate(`/dataset/${prefix}/${record.id}`); }; @@ -176,7 +177,7 @@ function ResourceList( onClick={showModal} icon={} > - {resourceConfig[resourceType].addBtnTitle} + {config.addBtnTitle} )}
diff --git a/react-ui/src/pages/Dataset/components/ResourcePage/index.tsx b/react-ui/src/pages/Dataset/components/ResourcePage/index.tsx index d5e1ed43..e5180a01 100644 --- a/react-ui/src/pages/Dataset/components/ResourcePage/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourcePage/index.tsx @@ -21,6 +21,7 @@ function ResourcePage({ resourceType }: ResourcePageProps) { const [activeType, setActiveType] = useState(cacheState?.activeType); const [activeTag, setActiveTag] = useState(cacheState?.activeTag); const dataListRef = useRef(null); + const config = resourceConfig[resourceType]; useEffect(() => { getAssetIconList(); @@ -52,16 +53,10 @@ function ResourcePage({ resourceType }: ResourcePageProps) { if (res && res.data && res.data.content) { const { content } = res.data; setTypeList( - content.filter( - (item: CategoryData) => - Number(item.category_id) === resourceConfig[resourceType].typeValue, - ), + content.filter((item: CategoryData) => Number(item.category_id) === config.typeValue), ); setTagList( - content.filter( - (item: CategoryData) => - Number(item.category_id) === resourceConfig[resourceType].tagValue, - ), + content.filter((item: CategoryData) => Number(item.category_id) === config.tagValue), ); } }; @@ -76,11 +71,7 @@ function ResourcePage({ resourceType }: ResourcePageProps) { return (
- +
([]); const { message } = App.useApp(); + const config = resourceConfig[resourceType]; // 获取版本文件列表 useEffectWhen( @@ -59,9 +60,9 @@ function ResourceVersion({ const getFileList = async (version: string) => { const params = { version, - [resourceConfig[resourceType].fileReqParamKey]: resourceId, + [config.fileReqParamKey]: resourceId, }; - const request = resourceConfig[resourceType].getFiles; + const request = config.getFiles; const [res] = await to(request(params)); if (res) { setFileList(res?.data?.content ?? []); @@ -70,9 +71,9 @@ function ResourceVersion({ // 删除版本 const deleteVersion = async () => { - const request = resourceConfig[resourceType].deleteVersion; + const request = config.deleteVersion; const params = { - [resourceConfig[resourceType].idParamKey]: resourceId, + [config.idParamKey]: resourceId, version, }; const [res] = await to(request(params)); @@ -111,13 +112,13 @@ function ResourceVersion({ // 全部导出 const handleExport = async () => { - const url = resourceConfig[resourceType].downloadAllAction; + const url = config.downloadAllAction; downLoadZip(url, { models_id: resourceId, version }); }; // 单个导出 const downloadAlone = (record: ResourceFileData) => { - const url = resourceConfig[resourceType].downloadSingleAction; + const url = config.downloadSingleAction; downLoadZip(`${url}/${record.id}`); }; diff --git a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx index b92ab783..92b9e0b8 100644 --- a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx +++ b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx @@ -118,6 +118,7 @@ function ResourceSelectorModal({ const [fisrtLoadList, setFisrtLoadList] = useState(false); const [fisrtLoadVersions, setFisrtLoadVersions] = useState(false); const treeRef = useRef(null); + const config = selectorTypeConfig[type]; useEffect(() => { setExpandedKeys([]); @@ -143,9 +144,9 @@ function ResourceSelectorModal({ const params = { page: 0, size: 1000, - [selectorTypeConfig[type].litReqParamKey]: available_range, + [config.litReqParamKey]: available_range, }; - const getListReq = selectorTypeConfig[type].getList; + const getListReq = config.getList; const [res] = await to(getListReq(params)); if (res) { const list = res.data?.content || []; @@ -161,10 +162,10 @@ function ResourceSelectorModal({ // 获取数据集\模型\镜像版本列表 const getVersions = async (parentId: number) => { - const getVersionsReq = selectorTypeConfig[type].getVersions; + const getVersionsReq = config.getVersions; const [res, error] = await to(getVersionsReq(parentId)); if (res) { - const list = selectorTypeConfig[type].handleVersionResponse(res); + const list = config.handleVersionResponse(res); const children = list.map(convertVersionToTreeData(parentId)); // 更新 treeData children setOriginTreeData((prev) => prev.map(updateChildren(parentId, children))); @@ -183,8 +184,8 @@ function ResourceSelectorModal({ // 获取版本下的文件 const getFiles = async (id: number, version: string) => { - const getFilesReq = selectorTypeConfig[type].getFiles; - const paramsKey = selectorTypeConfig[type].fileReqParamKey; + const getFilesReq = config.getFiles; + const paramsKey = config.fileReqParamKey; const params = { version: version, [paramsKey]: id }; const [res] = await to(getFilesReq(params)); if (res) { @@ -282,14 +283,12 @@ function ResourceSelectorModal({ } }; - const title = `选择${selectorTypeConfig[type].name}`; - const palceholder = `请输入${selectorTypeConfig[type].name}名称`; + const title = `选择${config.name}`; + const palceholder = `请输入${config.name}名称`; const fileTitle = - type === ResourceSelectorType.Mirror - ? '已选镜像' - : `已选${selectorTypeConfig[type].name}文件(${files.length})`; - const tabItems = selectorTypeConfig[type].tabItems; - const titleImg = selectorTypeConfig[type].modalIcon; + type === ResourceSelectorType.Mirror ? '已选镜像' : `已选${config.name}文件(${files.length})`; + const tabItems = config.tabItems; + const titleImg = config.modalIcon; return ( From 5ac61d2cd674038cbeb7f161c0381af6ef617cc0 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Thu, 20 Jun 2024 11:09:06 +0800 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E7=8E=AF=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/assets/img/editor-parameter.png | Bin 0 -> 1264 bytes .../src/components/ParameterInput/index.tsx | 2 +- .../DevelopmentEnvironment/Create/index.less | 17 + .../DevelopmentEnvironment/Create/index.tsx | 344 ++++++++++++++++++ .../DevelopmentEnvironment/List/index.less | 22 ++ .../DevelopmentEnvironment/List/index.tsx | 263 +++++++++++++ .../components/EditorStatusCell/index.less | 19 + .../components/EditorStatusCell/index.tsx | 44 +++ react-ui/src/pages/Mirror/Info/index.less | 18 - react-ui/src/pages/Mirror/Info/index.tsx | 3 +- .../pages/ModelDeployment/Create/index.tsx | 36 +- .../services/developmentEnvironment/index.js | 16 - .../services/developmentEnvironment/index.ts | 59 +++ 13 files changed, 792 insertions(+), 51 deletions(-) create mode 100644 react-ui/src/assets/img/editor-parameter.png create mode 100644 react-ui/src/pages/DevelopmentEnvironment/Create/index.less create mode 100644 react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx create mode 100644 react-ui/src/pages/DevelopmentEnvironment/List/index.less create mode 100644 react-ui/src/pages/DevelopmentEnvironment/List/index.tsx create mode 100644 react-ui/src/pages/DevelopmentEnvironment/components/EditorStatusCell/index.less create mode 100644 react-ui/src/pages/DevelopmentEnvironment/components/EditorStatusCell/index.tsx delete mode 100644 react-ui/src/services/developmentEnvironment/index.js create mode 100644 react-ui/src/services/developmentEnvironment/index.ts diff --git a/react-ui/src/assets/img/editor-parameter.png b/react-ui/src/assets/img/editor-parameter.png new file mode 100644 index 0000000000000000000000000000000000000000..b5fd9f41a4a8abcc2174182549f408e41f6d3995 GIT binary patch literal 1264 zcmVDys!z_2rRaCs4zR)UzctzPhLfkv)cRxNqN;tvG}JVQ+xfg$0XkTYy4CA zeh9|Taw`GZ`P8FIJ~?D(fx^;1NzyVaHhh_lVOSa#Fj!zl_8H7XEpto>LO5um^*35w z+;3y@dR)FSfbiHL>_U}EQo?;$?#M;@4$I87PP?lV?#wSzK9Q~A@5TQk1{2*zD84p8 zP2m}_jE{=#gR+w(wb%ur`w3b~oD;J)#xVUH3Txv~`3!*Ne&Gp9pQ@5#W74vVRwONT zUR<>?hVz8;qQ~&PX)t2VkJ8@i0-{kNH7@=)*#^KE5L;|LpUn=94bn<3da}Blw0pe~ zxw%O<|8qUiTh@iFwGxsd1&9m-jzI%md%={Sm&=5VYg1bpnJAkg5KqBxqORPN5(Yp8S zw&!O))(^c)${R$*lXhmU!zXP5@nUe$G68?HD)Pb94~xNA66vihfH_i<{O)HM?T4;6*$Pj$q)BPmHRtnS$ zF=U1-+D1TXtx@xT`^z#Y9CSfSVkRR(&@qXyu4+1%D)*6^Q`&cEJ1PS>xQ<9cUhL9` zh0|Er$1JoTm7_Ws304L23=FLnXoNvEl#%TN((fSRTN&2Cg-Fr_77AHI!7P!3f>^jz zrSO>#i~fcJG4lb}%Kl_na8Bv1@ZvmXpa!O*_>F@Z+|8yFb| zBcI43y%aw$#(C;(&&@2Wp1m%9hfvDpYhN7C{yM}(K^bXj-S5_te@y2S(}CQd@A8$AHa%InBoQ7i-KlCSY6x8im>UKYX=}39 z8@cxW{jdQg1)FJ^_v331f-X<0x1{NVfL*RJF|aNf$$<+9bh8F;V4QPwJNSP auKxkknaVEgf$&QJ0000 diff --git a/react-ui/src/pages/DevelopmentEnvironment/Create/index.less b/react-ui/src/pages/DevelopmentEnvironment/Create/index.less new file mode 100644 index 00000000..cd1dcb27 --- /dev/null +++ b/react-ui/src/pages/DevelopmentEnvironment/Create/index.less @@ -0,0 +1,17 @@ +.editor-create { + height: 100%; + + &__content { + height: calc(100% - 60px); + margin-top: 10px; + padding: 30px 30px 10px; + overflow: auto; + background-color: white; + border-radius: 10px; + + &__type { + color: @text-color; + font-size: @font-size-input-lg; + } + } +} diff --git a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx new file mode 100644 index 00000000..036fc12c --- /dev/null +++ b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx @@ -0,0 +1,344 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-16 13:58:08 + * @Description: 创建镜像 + */ +import KFIcon from '@/components/KFIcon'; +import KFRadio, { type KFRadioItem } from '@/components/KFRadio'; +import PageTitle from '@/components/PageTitle'; +import ParameterInput from '@/components/ParameterInput'; +import SubAreaTitle from '@/components/SubAreaTitle'; +import { useComputingResource } from '@/hooks/resource'; +import ResourceSelectorModal, { + ResourceSelectorResponse, + ResourceSelectorType, + selectorTypeConfig, +} from '@/pages/Pipeline/components/ResourceSelectorModal'; +import { createEditorReq } from '@/services/developmentEnvironment'; +import { openAntdModal } from '@/utils/modal'; +import { to } from '@/utils/promise'; +import { useNavigate } from '@umijs/max'; +import { App, Button, Col, Form, Input, Row, Select } from 'antd'; +import { pick } from 'lodash'; +import { useState } from 'react'; +import styles from './index.less'; + +type FormData = { + name: string; + computing_resource: string; + standard: string; + image: string; + model: ResourceSelectorResponse; + dataset: ResourceSelectorResponse; +}; + +enum ComputingResourceType { + GPU = 'GPU', + NPU = 'NPU', +} + +const EditorRadioItems: KFRadioItem[] = [ + { + key: ComputingResourceType.GPU, + title: '英伟达GPU', + icon: , + }, + { + key: ComputingResourceType.NPU, + title: '昇腾NPU', + icon: , + }, +]; + +function EditorCreate() { + const navgite = useNavigate(); + const [form] = Form.useForm(); + const { message } = App.useApp(); + const [resourceStandardList, filterResourceStandard] = useComputingResource(); + const [selectedModel, setSelectedModel] = useState( + undefined, + ); // 选择的模型,为了再次打开时恢复原来的选择 + const [selectedDataset, setSelectedDataset] = useState( + undefined, + ); // 选择的数据集,为了再次打开时恢复原来的选择 + const [selectedMirror, setSelectedMirror] = useState( + undefined, + ); // 选择的镜像,为了再次打开时恢复原来的选择 + + // 创建编辑器 + const createEditor = async (formData: FormData) => { + // const { model, dataset } = formData; + // const params = { + // ...formData, + // model: JSON.stringify(omit(model, ['showValue'])), + // dataset: JSON.stringify(dataset, ['showValue']), + // }; + const [res] = await to(createEditorReq(formData)); + if (res) { + message.success('创建成功'); + navgite(-1); + } + }; + + // 提交 + const handleSubmit = (values: FormData) => { + createEditor(values); + }; + + // 取消 + const cancel = () => { + navgite(-1); + }; + // 获取选择数据集、模型后面按钮 icon + const getSelectBtnIcon = (type: ResourceSelectorType) => { + return ; + }; + + // 选择模型、镜像、数据集 + const selectResource = (name: string, type: ResourceSelectorType) => { + let resource: ResourceSelectorResponse | undefined; + switch (type) { + case ResourceSelectorType.Model: + resource = selectedModel; + break; + case ResourceSelectorType.Dataset: + resource = selectedDataset; + break; + default: + resource = selectedMirror; + break; + } + const { close } = openAntdModal(ResourceSelectorModal, { + type, + defaultExpandedKeys: resource ? [resource.id] : [], + defaultCheckedKeys: resource ? [`${resource.id}-${resource.version}`] : [], + defaultActiveTab: resource?.activeTab, + onOk: (res) => { + if (res) { + if (type === ResourceSelectorType.Mirror) { + form.setFieldValue(name, res.path); + setSelectedMirror(res); + } else { + const showValue = `${res.name}:${res.version}`; + form.setFieldValue(name, { + ...pick(res, ['id', 'version', 'path']), + showValue, + }); + if (type === ResourceSelectorType.Model) { + setSelectedModel(res); + } else if (type === ResourceSelectorType.Dataset) { + setSelectedDataset(res); + } + } + } else { + if (type === ResourceSelectorType.Model) { + setSelectedModel(undefined); + } else if (type === ResourceSelectorType.Dataset) { + setSelectedDataset(undefined); + } else if (type === ResourceSelectorType.Mirror) { + setSelectedMirror(undefined); + } + form.setFieldValue(name, ''); + } + close(); + }, + }); + }; + + return ( +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + form.validateFields()} + /> { const navgite = useNavigate(); - let contextMenu = {}; const locationParams = useParams(); //新版本获取路由参数接口 const graphRef = useRef(); const paramsDrawerRef = useRef(); @@ -31,10 +30,23 @@ const EditPipeline = () => { let dragSourceNode; useEffect(() => { - initMenu(); + initGraph(); getFirstWorkflow(locationParams.id); + + const changeSize = () => { + if (!graph || graph.get('destroyed')) return; + if (!graphRef.current) return; + graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight); + graph.fitView(); + }; + + window.addEventListener('resize', changeSize); + return () => { + window.removeEventListener('resize', changeSize); + }; }, []); + // 拖拽结束,添加新节点 const onDragEnd = (val) => { const { x, y } = val; const point = graph.getPointByClient(x, y); @@ -45,11 +57,14 @@ const EditPipeline = () => { y: point.y, id: val.component_name + '-' + s8(), isCluster: false, + formError: true, }; // console.log('model', model); graph.addItem('node', model, false); }; - const formChange = (val) => { + + // 节点数据发生变化 + const handleFormChange = (val) => { if (graph) { const data = graph.save(); const index = data.nodes.findIndex((item) => { @@ -68,6 +83,8 @@ const EditPipeline = () => { graph.translate(lastPoint.x - newPoint.x, lastPoint.y - newPoint.y); } }; + + // 保存 const savePipeline = async (val) => { const [res, error] = await to(paramsDrawerRef.current.validateFields()); if (error) { @@ -76,13 +93,6 @@ const EditPipeline = () => { return; } - const duplicateName = findFirstDuplicate(res.global_param || []); - if (duplicateName) { - message.error('全局参数配置有重复的参数名称:' + duplicateName); - openParamsDrawer(); - return; - } - // const [propsRes, propsError] = await to(propsRef.current.getFieldsValue()); // if (propsError) { // message.error('基本信息必填项需配置'); @@ -108,6 +118,8 @@ const EditPipeline = () => { }); }, 500); }; + + // 渲染数据 const getGraphData = (data) => { if (graph) { // 修改历史数据有蓝色边框的问题 @@ -122,6 +134,8 @@ const EditPipeline = () => { }, 500); } }; + + // 处理并行边,暂时没有用 const processParallelEdgesOnAnchorPoint = ( edges, offsetDiff = 15, @@ -242,11 +256,11 @@ const EditPipeline = () => { } return false; }; + + // 复制节点 const cloneElement = (item) => { - console.log(item); let data = graph.save(); const nodeId = s8(); - console.log(item.getModel()); data.nodes.push({ ...item.getModel(), label: item.getModel().label + '-copy', @@ -256,66 +270,22 @@ const EditPipeline = () => { }); graph.changeData(data); }; - const getFirstWorkflow = (val) => { - getWorkflowById(val).then((ret) => { - if (ret && ret.data) { - setGlobalParam(ret.data.global_param || []); - } - if (graph && ret.data && ret.data.dag) { - getGraphData(JSON.parse(ret.data.dag)); - } - }); - }; - // 上下文菜单 - const initMenu = () => { - // const selectedNodes = this.selectedNodes; - contextMenu = new G6.Menu({ - getContent(evt) { - const type = evt.item.getType(); - const cloneDisplay = type === 'node' ? 'block' : 'none'; - return ` -
    -
  • 复制
  • -
  • 删除
  • -
`; - }, - handleMenuClick: (target, item) => { - switch (target.getAttribute('code')) { - case 'delete': - graph.removeItem(item); - break; - case 'clone': - cloneElement(item); - break; - default: - break; - } - }, - // offsetX and offsetY include the padding of the parent container - // 需要加上父级容器的 padding-left 16 与自身偏移量 10 - offsetX: 16 + 10, - // 需要加上父级容器的 padding-top 24 、画布兄弟元素高度、与自身偏移量 10 - offsetY: 0, - // the types of items that allow the menu show up - // 在哪些类型的元素上响应 - itemTypes: ['node', 'edge'], - }); - initGraph(); + // 获取流水线详情 + const getFirstWorkflow = async (val) => { + const [res] = await to(getWorkflowById(val)); + if (res && res.data) { + const { global_param, dag } = res.data; + setGlobalParam(global_param || []); + if (dag) { + getGraphData(JSON.parse(dag)); + } + } }; + // 初始化图 const initGraph = () => { + const contextMenu = initMenu(); G6.registerNode( 'rect-node', { @@ -515,6 +485,7 @@ const EditPipeline = () => { }, cursor: 'pointer', lineWidth: 1, + lineAppendWidth: 4, opacity: 1, stroke: '#CDD0DC', radius: 1, @@ -527,19 +498,13 @@ const EditPipeline = () => { }, }, }, - defaultCombo: { - type: 'rect', - fixCollapseSize: 70, - style: { - fill: '#00e0ff0d', - stroke: '#00e0ff', - lineDash: [5, 10], - cursor: 'pointer', - }, - }, }); + + bindEvents(); + }; + + const bindEvents = () => { graph.on('node:click', (e) => { - e.stopPropagation(); if (e.target.get('name') !== 'anchor-point' && e.item) { // 获取所有的上游节点 const parentNodes = findAllParentNodes(graph, e.item); @@ -558,16 +523,6 @@ const EditPipeline = () => { type: targetAnchorIdx === 0 || targetAnchorIdx === 1 ? 'cubic-vertical' : 'cubic-horizontal', }); - - // update the curveOffset for parallel edges - // const edges = graph.save().edges; - // processParallelEdgesOnAnchorPoint(edges); - // graph.getEdges().forEach((edge, i) => { - // graph.updateItem(edge, { - // curveOffset: edges[i].curveOffset, - // curvePosition: edges[i].curvePosition, - // }); - // }); }); // 删除边时,修改 anchor-point 的 links 值 graph.on('afterremoveitem', (e) => { @@ -639,13 +594,56 @@ const EditPipeline = () => { graph.setItemState(e.item, 'drop', false); dropAnchorIdx = undefined; }); - window.onresize = () => { - if (!graph || graph.get('destroyed')) return; - if (!graphRef.current) return; - graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight); - graph.fitView(); - }; }; + + // 上下文菜单 + const initMenu = () => { + const contextMenu = new G6.Menu({ + getContent(evt) { + const type = evt.item.getType(); + const cloneDisplay = type === 'node' ? 'block' : 'none'; + return ` +
    +
  • 复制
  • +
  • 删除
  • +
`; + }, + handleMenuClick: (target, item) => { + switch (target.getAttribute('code')) { + case 'delete': + graph.removeItem(item); + break; + case 'clone': + cloneElement(item); + break; + default: + break; + } + }, + // offsetX and offsetY include the padding of the parent container + // 需要加上父级容器的 padding-left 16 与自身偏移量 10 + offsetX: 16 + 10, + // 需要加上父级容器的 padding-top 24 、画布兄弟元素高度、与自身偏移量 10 + offsetY: 0, + // the types of items that allow the menu show up + // 在哪些类型的元素上响应 + itemTypes: ['node', 'edge'], + }); + + return contextMenu; + }; + return (
@@ -687,7 +685,7 @@ const EditPipeline = () => {
- +