| @@ -16,6 +16,7 @@ | |||
| &__label { | |||
| position: relative; | |||
| flex: none; | |||
| color: @text-color-secondary; | |||
| text-align: justify; | |||
| text-align-last: justify; | |||
| @@ -26,6 +27,13 @@ | |||
| } | |||
| } | |||
| &__list-value { | |||
| display: flex; | |||
| flex: 1; | |||
| flex-direction: column; | |||
| gap: 5px 0; | |||
| } | |||
| &__value { | |||
| flex: 1; | |||
| margin-left: 16px; | |||
| @@ -1,14 +1,17 @@ | |||
| import { isEmptyString } from '@/utils'; | |||
| import { Link } from '@umijs/max'; | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| export type BasicInfoLink = { | |||
| value: string; | |||
| link?: string; | |||
| url?: string; | |||
| }; | |||
| export type BasicInfoData = { | |||
| label: string; | |||
| value?: any; | |||
| link?: string; | |||
| externalLink?: string; | |||
| format?: (_value?: any) => string | undefined; | |||
| format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; | |||
| }; | |||
| type BasicInfoProps = { | |||
| @@ -33,32 +36,23 @@ type BasicInfoItemProps = { | |||
| labelWidth?: number; | |||
| }; | |||
| function BasicInfoItem({ data, labelWidth = 100 }: BasicInfoItemProps) { | |||
| const { label, value, externalLink, link, format } = data; | |||
| const showValue = format ? format(value) : value; | |||
| const { label, value, format } = data; | |||
| const formatValue = format ? format(value) : value; | |||
| let valueComponent = undefined; | |||
| if (externalLink && showValue) { | |||
| if (Array.isArray(formatValue)) { | |||
| valueComponent = ( | |||
| <a | |||
| className="kf-basic-info-item__value kf-basic-info-item__link" | |||
| href={externalLink} | |||
| target="_blank" | |||
| rel="noopener noreferrer" | |||
| > | |||
| {showValue} | |||
| </a> | |||
| <div className="kf-basic-info-item__list-value"> | |||
| {formatValue.map((item: BasicInfoLink) => ( | |||
| <BasicInfoItemValue key={item.value} value={item.value} link={item.link} url={item.url} /> | |||
| ))} | |||
| </div> | |||
| ); | |||
| } else if (link && showValue) { | |||
| } else if (typeof formatValue === 'object' && formatValue) { | |||
| valueComponent = ( | |||
| <Link to={link} className="kf-basic-info-item__value kf-basic-info-item__link"> | |||
| {showValue} | |||
| </Link> | |||
| <BasicInfoItemValue value={formatValue.value} link={formatValue.link} url={formatValue.url} /> | |||
| ); | |||
| } else { | |||
| valueComponent = ( | |||
| <div className="kf-basic-info-item__value kf-basic-info-item__text"> | |||
| {isEmptyString(showValue) ? '--' : showValue} | |||
| </div> | |||
| ); | |||
| valueComponent = <BasicInfoItemValue value={formatValue} />; | |||
| } | |||
| return ( | |||
| <div className="kf-basic-info-item" key={label}> | |||
| @@ -70,4 +64,35 @@ function BasicInfoItem({ data, labelWidth = 100 }: BasicInfoItemProps) { | |||
| ); | |||
| } | |||
| type BasicInfoItemValueProps = { | |||
| value: string; | |||
| link?: string; | |||
| url?: string; | |||
| }; | |||
| function BasicInfoItemValue({ value, link, url }: BasicInfoItemValueProps) { | |||
| if (url && value) { | |||
| return ( | |||
| <a | |||
| className="kf-basic-info-item__value kf-basic-info-item__link" | |||
| href={url} | |||
| target="_blank" | |||
| rel="noopener noreferrer" | |||
| > | |||
| {value} | |||
| </a> | |||
| ); | |||
| } else if (link && value) { | |||
| return ( | |||
| <Link to={link} className="kf-basic-info-item__value kf-basic-info-item__link"> | |||
| {value} | |||
| </Link> | |||
| ); | |||
| } else { | |||
| return ( | |||
| <div className="kf-basic-info-item__value kf-basic-info-item__text">{value || '--'}</div> | |||
| ); | |||
| } | |||
| } | |||
| export default BasicInfo; | |||
| @@ -1,6 +1,14 @@ | |||
| import BasicInfo, { BasicInfoData } from '@/components/BasicInfo'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { DatasetData, ModelData, ResourceType } from '@/pages/Dataset/config'; | |||
| import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; | |||
| import { | |||
| DataSource, | |||
| DatasetData, | |||
| ModelData, | |||
| ProjectDependency, | |||
| ResourceType, | |||
| TrainTask, | |||
| } from '@/pages/Dataset/config'; | |||
| import styles from './index.less'; | |||
| type ResourceIntroProps = { | |||
| @@ -8,29 +16,80 @@ type ResourceIntroProps = { | |||
| info: DatasetData | ModelData; | |||
| }; | |||
| // const formatArray = (arr?: ResourceData[]) => { | |||
| // if (!arr || arr.length === 0) { | |||
| // return '--'; | |||
| // } | |||
| // return arr.map((item) => item.name).join('\n'); | |||
| // }; | |||
| const formatDataset = (datasets?: DatasetData[]) => { | |||
| if (!datasets || datasets.length === 0) { | |||
| return undefined; | |||
| } | |||
| return datasets.map((item) => ({ | |||
| value: item.name, | |||
| url: `${origin}/dataset/dataset/info/${item.id}?tab=${ResourceInfoTabKeys.Version}&version=${item.version}&name=${item.name}&owner=${item.owner}&identifier=${item.identifier}`, | |||
| })); | |||
| }; | |||
| const formatDataset = (arr?: DatasetData[]) => { | |||
| if (!arr || arr.length === 0) { | |||
| const formatParams = (map?: Record<string, string>, space: string = '') => { | |||
| if (!map || Object.keys(map).length === 0) { | |||
| return undefined; | |||
| } | |||
| return arr.map((item) => item.name).join('\n'); | |||
| return Object.entries(map) | |||
| .map(([key, value]) => `${space}${key} : ${value}`) | |||
| .join('\n'); | |||
| }; | |||
| const formatMap = (map?: Record<string, string>) => { | |||
| const formatMetrics = (map?: Record<string, string>) => { | |||
| if (!map || Object.keys(map).length === 0) { | |||
| return undefined; | |||
| } | |||
| return Object.entries(map) | |||
| .map(([key, value]) => `${key} = ${value}`) | |||
| .map(([key, value]) => { | |||
| if (typeof value === 'object' && value !== null) { | |||
| return `${key} : \n${formatParams(value, ' ')}`; | |||
| } | |||
| return `${key} : ${value}`; | |||
| }) | |||
| .join('\n'); | |||
| }; | |||
| const getProjectUrl = (project?: ProjectDependency) => { | |||
| if (!project || !project.url || !project.branch) { | |||
| return undefined; | |||
| } | |||
| const { url, branch } = project; | |||
| if (url.endsWith('.git')) { | |||
| return `${url.substring(0, url.length - 4)}/tree/${branch}`; | |||
| } | |||
| }; | |||
| const formatProject = (project?: ProjectDependency) => { | |||
| if (!project) { | |||
| return undefined; | |||
| } | |||
| return { | |||
| value: project.name, | |||
| url: getProjectUrl(project), | |||
| }; | |||
| }; | |||
| const formatTrainTask = (task?: TrainTask) => { | |||
| if (!task) { | |||
| return undefined; | |||
| } | |||
| return { | |||
| value: task.name, | |||
| url: `${origin}/pipeline/experiment/instance/${task.workflow_id}/${task.ins_id}`, | |||
| }; | |||
| }; | |||
| const formatSource = (source?: string) => { | |||
| if (source === DataSource.Create) { | |||
| return '用户上传'; | |||
| } else if (source === DataSource.HandExport) { | |||
| return '手动导入'; | |||
| } else if (source === DataSource.AtuoExport) { | |||
| return '自动导入'; | |||
| } | |||
| return source; | |||
| }; | |||
| const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [ | |||
| { | |||
| label: '数据集名称', | |||
| @@ -51,6 +110,12 @@ const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [ | |||
| { | |||
| label: '数据来源', | |||
| value: data.dataset_source, | |||
| format: formatSource, | |||
| }, | |||
| { | |||
| label: '训练任务', | |||
| value: data.train_task, | |||
| format: formatTrainTask, | |||
| }, | |||
| { | |||
| label: '处理代码', | |||
| @@ -97,7 +162,8 @@ const getModelDatas = (data: ModelData): BasicInfoData[] => [ | |||
| }, | |||
| { | |||
| label: '训练代码', | |||
| value: data.code, | |||
| value: data.project_depency, | |||
| format: formatProject, | |||
| }, | |||
| { | |||
| label: '训练数据集', | |||
| @@ -112,24 +178,22 @@ const getModelDatas = (data: ModelData): BasicInfoData[] => [ | |||
| { | |||
| label: '参数', | |||
| value: data.params, | |||
| format: formatMap, | |||
| format: formatParams, | |||
| }, | |||
| { | |||
| label: '指标', | |||
| value: data.metrics, | |||
| format: formatMap, | |||
| }, | |||
| { | |||
| label: '训练任务', | |||
| value: data.train_task, | |||
| format: (value?: any) => value?.name, | |||
| externalLink: data.train_task | |||
| ? `${location.origin}/pipeline/experiment/instance/${data.train_task.task_id}/${data.train_task.ins_id}` | |||
| : '', | |||
| format: formatMetrics, | |||
| }, | |||
| { | |||
| label: '模型来源', | |||
| value: data.model_source, | |||
| format: formatSource, | |||
| }, | |||
| { | |||
| label: '训练任务', | |||
| value: data.train_task, | |||
| format: formatTrainTask, | |||
| }, | |||
| { | |||
| label: '模型框架', | |||
| @@ -22,7 +22,8 @@ export enum ResourceType { | |||
| } | |||
| export enum DataSource { | |||
| Export = 'export', // 导出 | |||
| AtuoExport = 'auto_export', // 自动导出 | |||
| HandExport = 'hand_export', // 手动导出 | |||
| Create = 'add', // 新增 | |||
| } | |||
| @@ -137,6 +138,7 @@ export type CategoryData = { | |||
| // 数据集、模型列表数据 | |||
| export interface ResourceData { | |||
| resourceType: ResourceType.Dataset | ResourceType.Model; // 用于 ts 类型判断 | |||
| id: number; | |||
| name: string; | |||
| identifier: string; | |||
| @@ -150,7 +152,7 @@ export interface ResourceData { | |||
| version_desc?: string; | |||
| usage?: string; | |||
| relative_paths?: string; | |||
| resourceType: ResourceType.Dataset | ResourceType.Model; | |||
| train_task?: TrainTask; // 训练任务 | |||
| } | |||
| // 数据集数据 | |||
| @@ -174,7 +176,7 @@ export interface ModelData extends ResourceData { | |||
| test_datasets?: DatasetData[]; // 测试数据集 | |||
| params?: Record<string, string>; // 参数 | |||
| metrics?: Record<string, string>; // 指标 | |||
| train_task?: TrainTask; // 训练任务 | |||
| project_depency?: ProjectDependency; // 项目依赖 | |||
| model_source?: string; // 模型来源 | |||
| model_version_vos?: ResourceFileData[]; | |||
| } | |||
| @@ -199,5 +201,13 @@ export type ResourceFileData = { | |||
| export type TrainTask = { | |||
| ins_id: number; | |||
| name: string; | |||
| task_id: string; | |||
| experiment_id: number; | |||
| workflow_id: number; | |||
| }; | |||
| // 项目依赖 | |||
| export type ProjectDependency = { | |||
| url: string; | |||
| name: string; | |||
| branch: string; | |||
| }; | |||
| @@ -504,6 +504,9 @@ function ExperimentText() { | |||
| key={experimentNodeData.id} | |||
| open={propsDrawerOpen} | |||
| onClose={closePropsDrawer} | |||
| pipelineId={Number(locationParams.workflowId)} | |||
| experimentId={experimentIns.experiment_id} | |||
| experimentName={experimentIns.experiment_name} | |||
| instanceId={experimentIns.id} | |||
| instanceName={experimentIns.argo_ins_name} | |||
| instanceNamespace={experimentIns.argo_ins_ns} | |||
| @@ -13,6 +13,9 @@ import styles from './index.less'; | |||
| type ExperimentDrawerProps = { | |||
| open: boolean; | |||
| onClose: () => void; | |||
| pipelineId: number; // 流水线 id | |||
| experimentId: number; // 实验 id | |||
| experimentName: string; // 实验 name | |||
| instanceId: number; // 实验实例 id | |||
| instanceName: string; // 实验实例 name | |||
| instanceNamespace: string; // 实验实例 namespace | |||
| @@ -26,6 +29,9 @@ type ExperimentDrawerProps = { | |||
| const ExperimentDrawer = ({ | |||
| open, | |||
| onClose, | |||
| pipelineId, | |||
| experimentId, | |||
| experimentName, | |||
| instanceId, | |||
| instanceName, | |||
| instanceNamespace, | |||
| @@ -64,6 +70,9 @@ const ExperimentDrawer = ({ | |||
| label: '输出结果', | |||
| children: ( | |||
| <ExperimentResult | |||
| pipelineId={pipelineId} | |||
| experimentId={experimentId} | |||
| experimentName={experimentName} | |||
| experimentInsId={instanceId} | |||
| pipelineNodeId={instanceNodeData.id} | |||
| ></ExperimentResult> | |||
| @@ -8,6 +8,9 @@ import ExportModelModal from '../ExportModelModal'; | |||
| import styles from './index.less'; | |||
| type ExperimentResultProps = { | |||
| pipelineId: number; // 流水线 id | |||
| experimentId: number; // 实验 id | |||
| experimentName: string; // 实验 name | |||
| experimentInsId: number; // 实验实例 id | |||
| pipelineNodeId: string; // 流水线节点 id | |||
| }; | |||
| @@ -22,7 +25,13 @@ type ExperimentResultData = { | |||
| }[]; | |||
| }; | |||
| function ExperimentResult({ experimentInsId, pipelineNodeId }: ExperimentResultProps) { | |||
| function ExperimentResult({ | |||
| pipelineId, | |||
| experimentId, | |||
| experimentName, | |||
| experimentInsId, | |||
| pipelineNodeId, | |||
| }: ExperimentResultProps) { | |||
| const { message } = App.useApp(); | |||
| const [experimentResults, setExperimentResults] = useState<ExperimentResultData[]>([]); | |||
| @@ -46,6 +55,11 @@ function ExperimentResult({ experimentInsId, pipelineNodeId }: ExperimentResultP | |||
| // 导出到模型库 | |||
| const exportToModel = (path: string) => { | |||
| const { close } = openAntdModal(ExportModelModal, { | |||
| pipelineId, | |||
| experimentId, | |||
| experimentName, | |||
| experimentInsId, | |||
| pipelineNodeId, | |||
| path, | |||
| onOk: () => { | |||
| message.success('导出成功'); | |||
| @@ -1,12 +1,7 @@ | |||
| import editExperimentIcon from '@/assets/img/edit-experiment.png'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { ResourceVersionData, type ResourceData } from '@/pages/Dataset/config'; | |||
| import { | |||
| addModelVersion, | |||
| exportModelReq, | |||
| getModelList, | |||
| getModelVersionList, | |||
| } from '@/services/dataset'; | |||
| import { DataSource, ResourceVersionData, type ResourceData } from '@/pages/Dataset/config'; | |||
| import { addModelVersion, getModelList, getModelVersionList } from '@/services/dataset'; | |||
| import { to } from '@/utils/promise'; | |||
| import { InfoCircleOutlined } from '@ant-design/icons'; | |||
| import { Form, Input, ModalProps, Select } from 'antd'; | |||
| @@ -15,34 +10,48 @@ import { useEffect, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| type FormData = { | |||
| models_id: string; | |||
| id: number; | |||
| version: string; | |||
| description: string; | |||
| version_desc: string; | |||
| }; | |||
| type ExportModelResponce = { | |||
| fileName: string; | |||
| fileSize: string; | |||
| url: string; | |||
| }; | |||
| // type ExportModelResponce = { | |||
| // fileName: string; | |||
| // fileSize: string; | |||
| // url: string; | |||
| // }; | |||
| type CreateModelVersionParams = FormData & { | |||
| file_name: string; | |||
| file_size: string; | |||
| url: string; | |||
| // name: string; | |||
| }; | |||
| // type CreateModelVersionParams = FormData & { | |||
| // file_name: string; | |||
| // file_size: string; | |||
| // url: string; | |||
| // // name: string; | |||
| // }; | |||
| interface ExportModelModalProps extends Omit<ModalProps, 'onOk'> { | |||
| path: string; | |||
| pipelineId: number; // 流水线 id | |||
| experimentId: number; // 实验 id | |||
| experimentName: string; // 实验 name | |||
| experimentInsId: number; // 实验实例 id | |||
| pipelineNodeId: string; // 流水线节点 id | |||
| path: string; // 文件路径 | |||
| onOk: () => void; | |||
| } | |||
| function ExportModelModal({ path, onOk, ...rest }: ExportModelModalProps) { | |||
| function ExportModelModal({ | |||
| pipelineId, | |||
| experimentId, | |||
| experimentName, | |||
| experimentInsId, | |||
| pipelineNodeId, | |||
| path, | |||
| onOk, | |||
| ...rest | |||
| }: ExportModelModalProps) { | |||
| const [form] = Form.useForm(); | |||
| const [models, setModels] = useState<ResourceData[]>([]); | |||
| const [versions, setVersions] = useState<ResourceVersionData[]>([]); | |||
| const [uuid] = useState(Date.now()); | |||
| // const [uuid] = useState(Date.now()); | |||
| const layout = { | |||
| labelCol: { span: 24 }, | |||
| @@ -53,10 +62,19 @@ function ExportModelModal({ path, onOk, ...rest }: ExportModelModalProps) { | |||
| requestModelList(); | |||
| }, []); | |||
| // 模型版本tooltip | |||
| // 获取选中的模型 | |||
| const getSelectedModel = (id: number | undefined) => { | |||
| if (id) { | |||
| return models.find((item) => item.id === id); | |||
| } | |||
| return undefined; | |||
| }; | |||
| // 模型版本 tooltip | |||
| const getTooltip = () => { | |||
| const id = form.getFieldValue('models_id'); | |||
| const name = models.find((item) => item.id === id)?.name ?? ''; | |||
| const id = form.getFieldValue('id'); | |||
| const model = getSelectedModel(id); | |||
| const name = model?.name ?? ''; | |||
| const versionNames = versions.map((item: ResourceVersionData) => item.name).join('、'); | |||
| const tooltip = | |||
| versions.length > 0 ? `${name}有以下版本:\n${versionNames}\n注意不能重复` : undefined; | |||
| @@ -87,7 +105,7 @@ function ExportModelModal({ path, onOk, ...rest }: ExportModelModalProps) { | |||
| // 获取模型版本列表 | |||
| const getModelVersions = async (id: number) => { | |||
| const model = models.find((item) => item.id === id); | |||
| const model = getSelectedModel(id); | |||
| if (!model) { | |||
| return; | |||
| } | |||
| @@ -104,26 +122,26 @@ function ExportModelModal({ path, onOk, ...rest }: ExportModelModalProps) { | |||
| // 导出到模型 | |||
| const exportToModel = async (formData: FormData) => { | |||
| const id = form.getFieldValue('id'); | |||
| const model = getSelectedModel(id); | |||
| const params = { | |||
| uuid: String(uuid), | |||
| path, | |||
| ...formData, | |||
| identifier: model?.identifier, | |||
| name: model?.name, | |||
| model_source: DataSource.HandExport, | |||
| train_task: { | |||
| workflow_id: pipelineId, | |||
| experiment_id: experimentId, | |||
| name: experimentName, | |||
| ins_id: experimentInsId, | |||
| task_id: pipelineNodeId, | |||
| }, | |||
| model_version_vos: [ | |||
| { | |||
| url: 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(addModelVersion(params)); | |||
| if (res) { | |||
| onOk(); | |||
| @@ -152,11 +170,7 @@ function ExportModelModal({ path, onOk, ...rest }: ExportModelModalProps) { | |||
| labelAlign="left" | |||
| labelWrap | |||
| > | |||
| <Form.Item | |||
| label="模型名称" | |||
| name="models_id" | |||
| rules={[{ required: true, message: '请选择模型' }]} | |||
| > | |||
| <Form.Item label="模型名称" name="id" rules={[{ required: true, message: '请选择模型' }]}> | |||
| <Select | |||
| placeholder="请选择模型" | |||
| onChange={handleModelChange} | |||
| @@ -194,7 +208,7 @@ function ExportModelModal({ path, onOk, ...rest }: ExportModelModalProps) { | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="版本描述" | |||
| name="description" | |||
| name="version_desc" | |||
| rules={[{ required: true, message: '请输入版本描述' }]} | |||
| > | |||
| <Input.TextArea | |||
| @@ -16,7 +16,7 @@ function ModelInfo({ resourceId, data, onVersionChange }: ModelInfoProps) { | |||
| const gotoExperimentPage = () => { | |||
| if (data.model_meta.train_task?.ins_id) { | |||
| const { origin } = location; | |||
| const url = `${origin}/pipeline/experiment/instance/${data.model_meta.train_task.task_id}/${data.model_meta.train_task.ins_id}`; | |||
| const url = `${origin}/pipeline/experiment/instance/${data.model_meta.train_task.workflow_id}/${data.model_meta.train_task.ins_id}`; | |||
| window.open(url, '_blank'); | |||
| } | |||
| }; | |||
| @@ -60,12 +60,12 @@ function ModelInfo({ resourceId, data, onVersionChange }: ModelInfoProps) { | |||
| {data.model_meta.model_type || '--'} | |||
| </span> | |||
| </div> | |||
| <div className={styles['node-tooltips__row']}> | |||
| {/* <div className={styles['node-tooltips__row']}> | |||
| <span className={styles['node-tooltips__row__title']}>模型大小:</span> | |||
| <span className={styles['node-tooltips__row__value']}> | |||
| {data.model_meta.file_size || '--'} | |||
| </span> | |||
| </div> | |||
| </div> */} | |||
| <div className={styles['node-tooltips__row']}> | |||
| <span className={styles['node-tooltips__row__title']}>创建时间:</span> | |||
| <span className={styles['node-tooltips__row__value']}> | |||
| @@ -126,8 +126,12 @@ function DatasetInfo({ data }: { data: TrainDataset }) { | |||
| function ProjectInfo({ data }: { data: ProjectDependency }) { | |||
| const gotoProjectPage = () => { | |||
| const { url } = data; | |||
| window.open(url, '_blank'); | |||
| const { url, branch } = data; | |||
| let projectUrl = url; | |||
| if (url.endsWith('.git')) { | |||
| projectUrl = `${url.substring(0, url.length - 4)}/tree/${branch}`; | |||
| } | |||
| window.open(projectUrl, '_blank'); | |||
| }; | |||
| return ( | |||