| @@ -1,5 +1,5 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { AutoMLTaskType, ExperimentStatus } from '@/enums'; | |||||
| import { getActiveLearnInsReq } from '@/services/activeLearn'; | import { getActiveLearnInsReq } from '@/services/activeLearn'; | ||||
| import { NodeStatus } from '@/types'; | import { NodeStatus } from '@/types'; | ||||
| import { parseJsonText } from '@/utils'; | import { parseJsonText } from '@/utils'; | ||||
| @@ -169,13 +169,20 @@ function ActiveLearnInstance() { | |||||
| key: TabKeys.Result, | key: TabKeys.Result, | ||||
| label: '实验结果', | label: '实验结果', | ||||
| icon: <KFIcon type="icon-shiyanjieguo1" />, | icon: <KFIcon type="icon-shiyanjieguo1" />, | ||||
| children: <ExperimentResult fileUrl={instanceInfo?.result_txt} />, | |||||
| children: ( | |||||
| <ExperimentResult fileUrl={instanceInfo?.data} modelPath={instanceInfo?.result_path} /> | |||||
| ), | |||||
| }, | }, | ||||
| { | { | ||||
| key: TabKeys.History, | key: TabKeys.History, | ||||
| label: '寻优列表', | |||||
| label: '训练列表', | |||||
| icon: <KFIcon type="icon-Trialliebiao" />, | icon: <KFIcon type="icon-Trialliebiao" />, | ||||
| children: <ExperimentHistory trialList={instanceInfo?.trial_list ?? []} />, | |||||
| children: ( | |||||
| <ExperimentHistory | |||||
| trialList={instanceInfo?.trial_list ?? []} | |||||
| isClassification={experimentInfo?.task_type === AutoMLTaskType.Classification} | |||||
| /> | |||||
| ), | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -1,112 +1,89 @@ | |||||
| import { getFileReq } from '@/services/file'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import tableCellRender from '@/utils/table'; | |||||
| import { Table, type TableProps } from 'antd'; | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import { ActiveLearnTrial } from '@/pages/ActiveLearn/types'; | |||||
| import TrialFileTree from '@/pages/HyperParameter/components/TrialFileTree'; | |||||
| import { downloadCommonFile, MimeType } from '@/utils/downloadfile'; | |||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||||
| import { Button, Table, type TableProps } from 'antd'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useEffect, useState } from 'react'; | |||||
| import { useState } from 'react'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type ExperimentHistoryProps = { | type ExperimentHistoryProps = { | ||||
| fileUrl?: string; | |||||
| trialList: ActiveLearnTrial[]; | |||||
| isClassification: boolean; | isClassification: boolean; | ||||
| }; | }; | ||||
| type TableData = { | |||||
| id?: string; | |||||
| accuracy?: number; | |||||
| duration?: number; | |||||
| train_loss?: number; | |||||
| status?: string; | |||||
| feature?: string; | |||||
| althorithm?: string; | |||||
| }; | |||||
| function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) { | |||||
| const [tableData, setTableData] = useState<TableData[]>([]); | |||||
| useEffect(() => { | |||||
| if (fileUrl) { | |||||
| getHistoryFile(); | |||||
| } | |||||
| }, [fileUrl]); | |||||
| // 获取实验运行历史记录 | |||||
| const getHistoryFile = async () => { | |||||
| const [res] = await to(getFileReq(fileUrl)); | |||||
| if (res) { | |||||
| const data: any[] = res.data; | |||||
| const list: TableData[] = data.map((item) => { | |||||
| return { | |||||
| id: item[0]?.[0], | |||||
| accuracy: item[1]?.[5]?.accuracy, | |||||
| duration: item[1]?.[5]?.duration, | |||||
| train_loss: item[1]?.[5]?.train_loss, | |||||
| status: item[1]?.[2]?.['__enum__']?.split('.')?.[1], | |||||
| }; | |||||
| }); | |||||
| list.forEach((item) => { | |||||
| if (!item.id) return; | |||||
| const config = (res as any).configs?.[item.id]; | |||||
| item.feature = config?.['feature_preprocessor:__choice__']; | |||||
| item.althorithm = isClassification | |||||
| ? config?.['classifier:__choice__'] | |||||
| : config?.['regressor:__choice__']; | |||||
| }); | |||||
| setTableData(list); | |||||
| } | |||||
| }; | |||||
| const columns: TableProps<TableData>['columns'] = [ | |||||
| { | |||||
| title: 'ID', | |||||
| dataIndex: 'id', | |||||
| key: 'id', | |||||
| width: 80, | |||||
| render: tableCellRender(false), | |||||
| }, | |||||
| { | |||||
| title: '准确率', | |||||
| dataIndex: 'accuracy', | |||||
| key: 'accuracy', | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | |||||
| }, | |||||
| function ExperimentHistory({ trialList, isClassification }: ExperimentHistoryProps) { | |||||
| const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]); | |||||
| const columns: TableProps<ActiveLearnTrial>['columns'] = [ | |||||
| { | { | ||||
| title: '耗时', | |||||
| dataIndex: 'duration', | |||||
| key: 'duration', | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | |||||
| title: '序号', | |||||
| dataIndex: 'index', | |||||
| key: 'index', | |||||
| width: 100, | |||||
| render: tableCellRender(false, TableCellValueType.Index), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '训练损失', | |||||
| dataIndex: 'train_loss', | |||||
| key: 'train_loss', | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | |||||
| title: '查询数据量', | |||||
| dataIndex: 'n_instances', | |||||
| key: 'n_instances', | |||||
| render: tableCellRender(false), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '特征处理', | |||||
| dataIndex: 'feature', | |||||
| key: 'feature', | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | |||||
| title: '总数据量', | |||||
| dataIndex: 'data_num', | |||||
| key: 'data_num', | |||||
| render: tableCellRender(false), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '算法', | |||||
| dataIndex: 'althorithm', | |||||
| key: 'althorithm', | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | |||||
| title: isClassification ? '准确率' : '均方误差', | |||||
| dataIndex: isClassification ? 'accuracy' : 'mse', | |||||
| key: 'accuracy', | |||||
| render: tableCellRender(false), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '状态', | |||||
| dataIndex: 'status', | |||||
| key: 'status', | |||||
| width: 120, | |||||
| render: tableCellRender(false), | |||||
| title: '查询数据 id', | |||||
| dataIndex: 'query_idx', | |||||
| key: 'query_idx', | |||||
| render: (query_idx: string) => ( | |||||
| <div> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="start" | |||||
| icon={<KFIcon type="icon-xiazai" />} | |||||
| onClick={() => { | |||||
| if (query_idx) { | |||||
| const fileName = query_idx.split('/').slice(-1)[0]; | |||||
| let url = query_idx; | |||||
| if (process.env.NODE_ENV === 'development') { | |||||
| url = query_idx.replace('172.168.15.197:31213', 'localhost:8000'); | |||||
| } | |||||
| downloadCommonFile(url, MimeType.JSON, fileName); | |||||
| } | |||||
| }} | |||||
| > | |||||
| 下载 | |||||
| </Button> | |||||
| </div> | |||||
| ), | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| // 自定义展开视图 | |||||
| const expandedRowRender = (record: ActiveLearnTrial) => { | |||||
| return <TrialFileTree title="训练结果" trial={record}></TrialFileTree>; | |||||
| }; | |||||
| // 展开实例 | |||||
| const handleExpandChange = (expanded: boolean, record: ActiveLearnTrial) => { | |||||
| if (expanded) { | |||||
| setExpandedRowKeys([record.trial_id]); | |||||
| } else { | |||||
| setExpandedRowKeys([]); | |||||
| } | |||||
| }; | |||||
| return ( | return ( | ||||
| <div className={styles['experiment-history']}> | <div className={styles['experiment-history']}> | ||||
| <div className={styles['experiment-history__content']}> | <div className={styles['experiment-history__content']}> | ||||
| @@ -117,11 +94,17 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps | |||||
| )} | )} | ||||
| > | > | ||||
| <Table | <Table | ||||
| dataSource={tableData} | |||||
| dataSource={trialList} | |||||
| columns={columns} | columns={columns} | ||||
| pagination={false} | pagination={false} | ||||
| scroll={{ y: 'calc(100% - 55px)' }} | scroll={{ y: 'calc(100% - 55px)' }} | ||||
| rowKey="id" | |||||
| rowKey="trial_id" | |||||
| expandable={{ | |||||
| expandedRowRender: expandedRowRender, | |||||
| onExpand: handleExpandChange, | |||||
| expandedRowKeys: expandedRowKeys, | |||||
| rowExpandable: (record: ActiveLearnTrial) => !!record.file, | |||||
| }} | |||||
| /> | /> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -1,81 +1,49 @@ | |||||
| import InfoGroup from '@/components/InfoGroup'; | import InfoGroup from '@/components/InfoGroup'; | ||||
| import { getFileReq } from '@/services/file'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import { Button, Image } from 'antd'; | |||||
| import { useEffect, useMemo, useState } from 'react'; | |||||
| import { downloadCommonFile, MimeType } from '@/utils/downloadfile'; | |||||
| import { Button } from 'antd'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type ExperimentResultProps = { | type ExperimentResultProps = { | ||||
| fileUrl?: string; | fileUrl?: string; | ||||
| imageUrl?: string; | |||||
| modelPath?: string; | modelPath?: string; | ||||
| }; | }; | ||||
| function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProps) { | |||||
| const [result, setResult] = useState<string | undefined>(''); | |||||
| const images = useMemo(() => { | |||||
| if (imageUrl) { | |||||
| return imageUrl.split(',').map((item) => item.trim()); | |||||
| } | |||||
| return []; | |||||
| }, [imageUrl]); | |||||
| useEffect(() => { | |||||
| if (fileUrl) { | |||||
| getResultFile(); | |||||
| } | |||||
| }, [fileUrl]); | |||||
| // 获取实验运行历史记录 | |||||
| const getResultFile = async () => { | |||||
| const [res] = await to(getFileReq(fileUrl)); | |||||
| if (res) { | |||||
| setResult(res as any as string); | |||||
| } | |||||
| }; | |||||
| function ExperimentResult({ fileUrl, modelPath }: ExperimentResultProps) { | |||||
| return ( | return ( | ||||
| <div className={styles['experiment-result']}> | <div className={styles['experiment-result']}> | ||||
| <InfoGroup title="实验结果" height={420} width="100%"> | |||||
| <div className={styles['experiment-result__text']}>{result}</div> | |||||
| <InfoGroup title="数据选择查看" width="100%"> | |||||
| {fileUrl && ( | |||||
| <div> | |||||
| <Button | |||||
| type="primary" | |||||
| onClick={() => { | |||||
| const fileName = 'data.json'; | |||||
| let url = fileUrl; | |||||
| if (process.env.NODE_ENV === 'development') { | |||||
| url = fileUrl.replace('172.168.15.197:31213', 'localhost:8000'); | |||||
| } | |||||
| downloadCommonFile(url, MimeType.JSON, fileName); | |||||
| }} | |||||
| > | |||||
| 数据选择下载 | |||||
| </Button> | |||||
| </div> | |||||
| )} | |||||
| </InfoGroup> | </InfoGroup> | ||||
| <InfoGroup title="可视化结果" style={{ margin: '16px 0' }}> | |||||
| <div className={styles['experiment-result__images']}> | |||||
| <Image.PreviewGroup | |||||
| preview={{ | |||||
| onChange: (current, prev) => | |||||
| console.log(`current index: ${current}, prev index: ${prev}`), | |||||
| }} | |||||
| > | |||||
| {images.map((item) => ( | |||||
| <Image | |||||
| key={item} | |||||
| className={styles['experiment-result__images__item']} | |||||
| src={item} | |||||
| height={248} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| ))} | |||||
| </Image.PreviewGroup> | |||||
| </div> | |||||
| <InfoGroup title="模型下载" style={{ margin: '16px 0' }}> | |||||
| {modelPath && ( | |||||
| <div> | |||||
| <Button | |||||
| type="primary" | |||||
| onClick={() => { | |||||
| window.location.href = modelPath; | |||||
| }} | |||||
| > | |||||
| 模型下载 | |||||
| </Button> | |||||
| </div> | |||||
| )} | |||||
| </InfoGroup> | </InfoGroup> | ||||
| {modelPath && ( | |||||
| <div className={styles['experiment-result__download']}> | |||||
| <span style={{ marginRight: '12px', color: '#606b7a' }}>文件名</span> | |||||
| <span>save_model.joblib</span> | |||||
| <Button | |||||
| type="primary" | |||||
| className={styles['experiment-result__download__btn']} | |||||
| onClick={() => { | |||||
| window.location.href = modelPath; | |||||
| }} | |||||
| > | |||||
| 模型下载 | |||||
| </Button> | |||||
| </div> | |||||
| )} | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -1,120 +0,0 @@ | |||||
| { | |||||
| "workflow-xwnb8": { | |||||
| "id": "workflow-xwnb8", | |||||
| "name": "workflow-xwnb8", | |||||
| "type": "DAG", | |||||
| "phase": "Failed", | |||||
| "children": [ | |||||
| "workflow-xwnb8-1083129199" | |||||
| ], | |||||
| "progress": "2/3", | |||||
| "startedAt": "2025-04-18T06:56:18Z", | |||||
| "finishedAt": "2025-04-18T06:57:32Z", | |||||
| "displayName": "workflow-xwnb8", | |||||
| "templateName": "ml-workflow", | |||||
| "outboundNodes": [ | |||||
| "workflow-xwnb8-1355608520" | |||||
| ], | |||||
| "templateScope": "local/workflow-xwnb8", | |||||
| "resourcesDuration": { | |||||
| "cpu": 42, | |||||
| "memory": 851, | |||||
| "nvidia.com/gpu": 10 | |||||
| } | |||||
| }, | |||||
| "git-clone-9d0c5965": { | |||||
| "id": "workflow-xwnb8-514970004", | |||||
| "name": "workflow-xwnb8.git-clone-9d0c5965", | |||||
| "type": "Pod", | |||||
| "phase": "Succeeded", | |||||
| "outputs": { | |||||
| "exitCode": "0", | |||||
| "artifacts": [ | |||||
| { | |||||
| "s3": { | |||||
| "key": "workflow-xwnb8/workflow-xwnb8-git-clone-9d0c5965-514970004/main.log" | |||||
| }, | |||||
| "name": "main-logs" | |||||
| } | |||||
| ] | |||||
| }, | |||||
| "children": [ | |||||
| "workflow-xwnb8-1355608520" | |||||
| ], | |||||
| "progress": "1/1", | |||||
| "startedAt": "2025-04-18T06:56:38Z", | |||||
| "boundaryID": "workflow-xwnb8", | |||||
| "finishedAt": "2025-04-18T06:56:49Z", | |||||
| "displayName": "git-clone-9d0c5965", | |||||
| "hostNodeName": "k8s-node01", | |||||
| "templateName": "git-clone-9d0c5965", | |||||
| "templateScope": "local/workflow-xwnb8", | |||||
| "resourcesDuration": { | |||||
| "cpu": 1, | |||||
| "memory": 11 | |||||
| } | |||||
| }, | |||||
| "git-clone-e28c560c": { | |||||
| "id": "workflow-xwnb8-1083129199", | |||||
| "name": "workflow-xwnb8.git-clone-e28c560c", | |||||
| "type": "Pod", | |||||
| "phase": "Succeeded", | |||||
| "outputs": { | |||||
| "exitCode": "0", | |||||
| "artifacts": [ | |||||
| { | |||||
| "s3": { | |||||
| "key": "workflow-xwnb8/workflow-xwnb8-git-clone-e28c560c-1083129199/main.log" | |||||
| }, | |||||
| "name": "main-logs" | |||||
| } | |||||
| ] | |||||
| }, | |||||
| "children": [ | |||||
| "workflow-xwnb8-514970004" | |||||
| ], | |||||
| "progress": "1/1", | |||||
| "startedAt": "2025-04-18T06:56:18Z", | |||||
| "boundaryID": "workflow-xwnb8", | |||||
| "finishedAt": "2025-04-18T06:56:27Z", | |||||
| "displayName": "git-clone-e28c560c", | |||||
| "hostNodeName": "k8s-node01", | |||||
| "templateName": "git-clone-e28c560c", | |||||
| "templateScope": "local/workflow-xwnb8", | |||||
| "resourcesDuration": { | |||||
| "cpu": 1, | |||||
| "memory": 11 | |||||
| } | |||||
| }, | |||||
| "active-learn-b708ed0b": { | |||||
| "id": "workflow-xwnb8-1355608520", | |||||
| "name": "workflow-xwnb8.active-learn-b708ed0b", | |||||
| "type": "Pod", | |||||
| "phase": "Failed", | |||||
| "message": "Error (exit code 1)", | |||||
| "outputs": { | |||||
| "exitCode": "1", | |||||
| "artifacts": [ | |||||
| { | |||||
| "s3": { | |||||
| "key": "workflow-xwnb8/workflow-xwnb8-active-learn-b708ed0b-1355608520/main.log" | |||||
| }, | |||||
| "name": "main-logs" | |||||
| } | |||||
| ] | |||||
| }, | |||||
| "progress": "0/1", | |||||
| "startedAt": "2025-04-18T06:57:00Z", | |||||
| "boundaryID": "workflow-xwnb8", | |||||
| "finishedAt": "2025-04-18T06:57:27Z", | |||||
| "displayName": "active-learn-b708ed0b", | |||||
| "hostNodeName": "k8s-node01", | |||||
| "templateName": "active-learn-b708ed0b", | |||||
| "templateScope": "local/workflow-xwnb8", | |||||
| "resourcesDuration": { | |||||
| "cpu": 40, | |||||
| "memory": 829, | |||||
| "nvidia.com/gpu": 10 | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,14 +1,7 @@ | |||||
| /* | |||||
| * @Author: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git | |||||
| * @Date: 2025-04-18 08:40:03 | |||||
| * @LastEditors: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git | |||||
| * @LastEditTime: 2025-04-18 11:30:21 | |||||
| * @FilePath: \ci4s\react-ui\src\pages\ActiveLearn\types.ts | |||||
| * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE | |||||
| */ | |||||
| import { type ParameterInputObject } from '@/components/ResourceSelect'; | import { type ParameterInputObject } from '@/components/ResourceSelect'; | ||||
| import { type NodeStatus } from '@/types'; | import { type NodeStatus } from '@/types'; | ||||
| import { AutoMLTaskType } from '@/enums'; | import { AutoMLTaskType } from '@/enums'; | ||||
| import { HyperParameterFile } from '@/pages/HyperParameter/types'; | |||||
| // 操作类型 | // 操作类型 | ||||
| export enum OperationType { | export enum OperationType { | ||||
| @@ -82,4 +75,18 @@ export type ActiveLearnInstanceData = { | |||||
| update_time: string; | update_time: string; | ||||
| finish_time: string; | finish_time: string; | ||||
| nodeStatus?: NodeStatus; | nodeStatus?: NodeStatus; | ||||
| data: string, | |||||
| trial_list?: ActiveLearnTrial[] | |||||
| }; | }; | ||||
| export type ActiveLearnTrial = { | |||||
| trial_id: string; | |||||
| n_instances?: number; | |||||
| data_num?: number; | |||||
| accuracy?: number; | |||||
| mse?: number; | |||||
| query_idx?: string | |||||
| file?: HyperParameterFile; | |||||
| }; | |||||
| export {type HyperParameterFile} | |||||
| @@ -159,6 +159,9 @@ function ExperimentInstanceComponent({ | |||||
| <Checkbox | <Checkbox | ||||
| checked={isSingleChecked(item.id)} | checked={isSingleChecked(item.id)} | ||||
| onChange={() => checkSingle(item.id)} | onChange={() => checkSingle(item.id)} | ||||
| disabled={ | |||||
| item.status === ExperimentStatus.Running || item.status === ExperimentStatus.Pending | |||||
| } | |||||
| ></Checkbox> | ></Checkbox> | ||||
| </div> | </div> | ||||
| <a | <a | ||||
| @@ -357,7 +357,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| return ( | return ( | ||||
| <div className={styles['experiment-list']}> | <div className={styles['experiment-list']}> | ||||
| <PageTitle title={config.title + '列表'}></PageTitle> | |||||
| <PageTitle title={config.title}></PageTitle> | |||||
| <div className={styles['experiment-list__content']}> | <div className={styles['experiment-list__content']}> | ||||
| <div className={styles['experiment-list__content__filter']}> | <div className={styles['experiment-list__content__filter']}> | ||||
| <Input.Search | <Input.Search | ||||
| @@ -55,19 +55,4 @@ | |||||
| .table-best-row { | .table-best-row { | ||||
| color: @success-color; | color: @success-color; | ||||
| font-weight: bold; | font-weight: bold; | ||||
| } | |||||
| .trail-result { | |||||
| :global { | |||||
| .ant-tree-node-selected { | |||||
| .trail-result__icon { | |||||
| color: white; | |||||
| } | |||||
| } | |||||
| .trail-result__icon { | |||||
| margin-left: 8px; | |||||
| color: @primary-color; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,19 +1,15 @@ | |||||
| import InfoGroup from '@/components/InfoGroup'; | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import TableColTitle from '@/components/TableColTitle'; | import TableColTitle from '@/components/TableColTitle'; | ||||
| import TrialStatusCell from '@/pages/HyperParameter/components/TrialStatusCell'; | import TrialStatusCell from '@/pages/HyperParameter/components/TrialStatusCell'; | ||||
| import { HyperParameterFile, HyperParameterTrial } from '@/pages/HyperParameter/types'; | |||||
| import { HyperParameterTrial } from '@/pages/HyperParameter/types'; | |||||
| import { getExpMetricsReq } from '@/services/hyperParameter'; | import { getExpMetricsReq } from '@/services/hyperParameter'; | ||||
| import { downLoadZip } from '@/utils/downloadfile'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | import tableCellRender, { TableCellValueType } from '@/utils/table'; | ||||
| import { App, Button, Table, Tree, type TableProps, type TreeDataNode } from 'antd'; | |||||
| import { App, Button, Table, type TableProps } from 'antd'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||
| import TrialFileTree from '../TrialFileTree'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| const { DirectoryTree } = Tree; | |||||
| type ExperimentHistoryProps = { | type ExperimentHistoryProps = { | ||||
| trialList?: HyperParameterTrial[]; | trialList?: HyperParameterTrial[]; | ||||
| }; | }; | ||||
| @@ -110,7 +106,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||||
| if (metricNames.length) { | if (metricNames.length) { | ||||
| trialColumns.push({ | trialColumns.push({ | ||||
| title: `指标分析(${first.metric ?? ''})`, | |||||
| title: `指标分析(${first?.metric ?? ''})`, | |||||
| dataIndex: 'metrics', | dataIndex: 'metrics', | ||||
| key: 'metrics', | key: 'metrics', | ||||
| align: 'center', | align: 'center', | ||||
| @@ -127,51 +123,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||||
| // 自定义展开视图 | // 自定义展开视图 | ||||
| const expandedRowRender = (record: HyperParameterTrial) => { | const expandedRowRender = (record: HyperParameterTrial) => { | ||||
| const filesToTreeData = ( | |||||
| files: HyperParameterFile[], | |||||
| parent?: HyperParameterFile, | |||||
| ): TreeDataNode[] => | |||||
| files.map((file) => { | |||||
| const key = parent ? `${parent.name}/${file.name}` : file.name; | |||||
| return { | |||||
| ...file, | |||||
| key, | |||||
| title: file.name, | |||||
| children: file.children ? filesToTreeData(file.children, file) : undefined, | |||||
| }; | |||||
| }); | |||||
| const treeData: TreeDataNode[] = filesToTreeData([record.file]); | |||||
| return ( | |||||
| <InfoGroup title="寻优结果" className={styles['trail-result']}> | |||||
| <DirectoryTree | |||||
| // @ts-ignore | |||||
| treeData={treeData} | |||||
| defaultExpandAll | |||||
| titleRender={(record: TreeDataNode & HyperParameterFile) => { | |||||
| const label = record.title + (record.isFile ? `(${record.size})` : ''); | |||||
| return ( | |||||
| <> | |||||
| <span style={{ fontSize: 14 }}>{label}</span> | |||||
| <KFIcon | |||||
| type="icon-xiazai" | |||||
| className="trail-result__icon" | |||||
| onClick={(e) => { | |||||
| e.stopPropagation(); | |||||
| downLoadZip( | |||||
| record.isFile | |||||
| ? `/api/mmp/minioStorage/downloadFile` | |||||
| : `/api/mmp/minioStorage/download`, | |||||
| { path: record.url }, | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| </InfoGroup> | |||||
| ); | |||||
| return <TrialFileTree title="寻优结果" trial={record}></TrialFileTree>; | |||||
| }; | }; | ||||
| // 展开实例 | // 展开实例 | ||||
| @@ -237,6 +189,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||||
| expandedRowRender: expandedRowRender, | expandedRowRender: expandedRowRender, | ||||
| onExpand: handleExpandChange, | onExpand: handleExpandChange, | ||||
| expandedRowKeys: expandedRowKeys, | expandedRowKeys: expandedRowKeys, | ||||
| rowExpandable: (record: HyperParameterTrial) => !!record.file, | |||||
| }} | }} | ||||
| rowSelection={rowSelection} | rowSelection={rowSelection} | ||||
| /> | /> | ||||
| @@ -0,0 +1,15 @@ | |||||
| .trail-file-tree { | |||||
| :global { | |||||
| .ant-tree-node-selected { | |||||
| .trail-file-tree__icon { | |||||
| color: white; | |||||
| } | |||||
| } | |||||
| .trail-file-tree__icon { | |||||
| margin-left: 8px; | |||||
| color: @primary-color; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,63 @@ | |||||
| import InfoGroup from '@/components/InfoGroup'; | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import { HyperParameterFile } from '@/pages/HyperParameter/types'; | |||||
| import { downLoadZip } from '@/utils/downloadfile'; | |||||
| import { Tree, type TreeDataNode } from 'antd'; | |||||
| import styles from './index.less'; | |||||
| const { DirectoryTree } = Tree; | |||||
| export type TrialFileTreeProps = { | |||||
| title: string; | |||||
| trial: { file?: HyperParameterFile }; | |||||
| }; | |||||
| function TrialFileTree({ trial, title }: TrialFileTreeProps) { | |||||
| const filesToTreeData = ( | |||||
| files: HyperParameterFile[], | |||||
| parent?: HyperParameterFile, | |||||
| ): TreeDataNode[] => | |||||
| files.map((file) => { | |||||
| const key = parent ? `${parent.name}/${file.name}` : file.name; | |||||
| return { | |||||
| ...file, | |||||
| key, | |||||
| title: file.name, | |||||
| children: file.children ? filesToTreeData(file.children, file) : undefined, | |||||
| }; | |||||
| }); | |||||
| const treeData: TreeDataNode[] = filesToTreeData(trial.file ? [trial.file] : []); | |||||
| return ( | |||||
| <InfoGroup title={title} className={styles['trail-file-tree']}> | |||||
| <DirectoryTree | |||||
| // @ts-ignore | |||||
| treeData={treeData} | |||||
| defaultExpandAll | |||||
| titleRender={(record: TreeDataNode & HyperParameterFile) => { | |||||
| const label = record.title + (record.isFile ? `(${record.size})` : ''); | |||||
| return ( | |||||
| <> | |||||
| <span style={{ fontSize: 14 }}>{label}</span> | |||||
| <KFIcon | |||||
| type="icon-xiazai" | |||||
| className="trail-file-tree__icon" | |||||
| onClick={(e) => { | |||||
| e.stopPropagation(); | |||||
| downLoadZip( | |||||
| record.isFile | |||||
| ? `/api/mmp/minioStorage/downloadFile` | |||||
| : `/api/mmp/minioStorage/download`, | |||||
| { path: record.url }, | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| </InfoGroup> | |||||
| ); | |||||
| } | |||||
| export default TrialFileTree; | |||||
| @@ -1,9 +1,10 @@ | |||||
| import { request } from '@umijs/max'; | import { request } from '@umijs/max'; | ||||
| /** MimeType */ | /** MimeType */ | ||||
| export const mimeMap = { | |||||
| xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | |||||
| zip: 'application/zip', | |||||
| export enum MimeType { | |||||
| XLSX = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | |||||
| ZIP = 'application/zip', | |||||
| JSON = 'application/json' | |||||
| }; | }; | ||||
| /** | /** | ||||
| @@ -11,7 +12,7 @@ export const mimeMap = { | |||||
| * @param res - blob响应内容 | * @param res - blob响应内容 | ||||
| * @param mimeType - MIME类型 | * @param mimeType - MIME类型 | ||||
| */ | */ | ||||
| export function resolveBlob(res: any, mimeType: string) { | |||||
| export function resolveBlob(res: any, mimeType: string, specifiedFileName: string = "file") { | |||||
| const aLink = document.createElement('a'); | const aLink = document.createElement('a'); | ||||
| const blob = new Blob([res.data], { type: mimeType }); | const blob = new Blob([res.data], { type: mimeType }); | ||||
| // 从response的headers中获取filename, | // 从response的headers中获取filename, | ||||
| @@ -20,7 +21,7 @@ export function resolveBlob(res: any, mimeType: string) { | |||||
| // console.log(res); | // console.log(res); | ||||
| const contentDisposition = decodeURI(res.headers['content-disposition']); | const contentDisposition = decodeURI(res.headers['content-disposition']); | ||||
| const result = patt.exec(contentDisposition); | const result = patt.exec(contentDisposition); | ||||
| let fileName = result ? result[1] : 'file'; | |||||
| let fileName = result ? result[1] : specifiedFileName; | |||||
| fileName = fileName.replace(/"/g, ''); | fileName = fileName.replace(/"/g, ''); | ||||
| aLink.style.display = 'none'; | aLink.style.display = 'none'; | ||||
| aLink.href = URL.createObjectURL(blob); | aLink.href = URL.createObjectURL(blob); | ||||
| @@ -43,7 +44,7 @@ export function downLoadZip(url: string, params?: any) { | |||||
| responseType: 'blob', | responseType: 'blob', | ||||
| getResponse: true, | getResponse: true, | ||||
| }).then((res) => { | }).then((res) => { | ||||
| resolveBlob(res, mimeMap.zip); | |||||
| resolveBlob(res, MimeType.ZIP); | |||||
| }); | }); | ||||
| } | } | ||||
| @@ -64,7 +65,30 @@ export async function downloadXlsx( | |||||
| responseType: 'blob', | responseType: 'blob', | ||||
| getResponse: true, | getResponse: true, | ||||
| }).then((res) => { | }).then((res) => { | ||||
| resolveBlob(res, mimeMap.xlsx); | |||||
| resolveBlob(res, MimeType.XLSX); | |||||
| }); | |||||
| } | |||||
| /** | |||||
| * 下载文本文件 | |||||
| * @param url - url 地址 | |||||
| * @param type - mime-type | |||||
| * @param fileName - 文件名 | |||||
| * @param method - 请求方法 | |||||
| * @param options - 请求选项 | |||||
| */ | |||||
| export function downloadCommonFile(url: string, type: string, fileName: string = "file", method: string = 'GET', options?: Record<string, any>) { | |||||
| request(url, { | |||||
| method: method, | |||||
| headers: { | |||||
| isToken: false, | |||||
| }, | |||||
| skipValidating: true, | |||||
| ...options, | |||||
| responseType: 'blob', | |||||
| getResponse: true, | |||||
| }).then((res) => { | |||||
| resolveBlob(res, type, fileName); | |||||
| }); | }); | ||||
| } | } | ||||