| @@ -1,5 +1,5 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { AutoMLTaskType, ExperimentStatus } from '@/enums'; | |||
| import { getActiveLearnInsReq } from '@/services/activeLearn'; | |||
| import { NodeStatus } from '@/types'; | |||
| import { parseJsonText } from '@/utils'; | |||
| @@ -169,13 +169,20 @@ function ActiveLearnInstance() { | |||
| key: TabKeys.Result, | |||
| label: '实验结果', | |||
| icon: <KFIcon type="icon-shiyanjieguo1" />, | |||
| children: <ExperimentResult fileUrl={instanceInfo?.result_txt} />, | |||
| children: ( | |||
| <ExperimentResult fileUrl={instanceInfo?.data} modelPath={instanceInfo?.result_path} /> | |||
| ), | |||
| }, | |||
| { | |||
| key: TabKeys.History, | |||
| label: '寻优列表', | |||
| label: '训练列表', | |||
| 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 { useEffect, useState } from 'react'; | |||
| import { useState } from 'react'; | |||
| import styles from './index.less'; | |||
| type ExperimentHistoryProps = { | |||
| fileUrl?: string; | |||
| trialList: ActiveLearnTrial[]; | |||
| 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 ( | |||
| <div className={styles['experiment-history']}> | |||
| <div className={styles['experiment-history__content']}> | |||
| @@ -117,11 +94,17 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps | |||
| )} | |||
| > | |||
| <Table | |||
| dataSource={tableData} | |||
| dataSource={trialList} | |||
| columns={columns} | |||
| pagination={false} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| rowKey="id" | |||
| rowKey="trial_id" | |||
| expandable={{ | |||
| expandedRowRender: expandedRowRender, | |||
| onExpand: handleExpandChange, | |||
| expandedRowKeys: expandedRowKeys, | |||
| rowExpandable: (record: ActiveLearnTrial) => !!record.file, | |||
| }} | |||
| /> | |||
| </div> | |||
| </div> | |||
| @@ -1,81 +1,49 @@ | |||
| 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'; | |||
| type ExperimentResultProps = { | |||
| fileUrl?: string; | |||
| imageUrl?: 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 ( | |||
| <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 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> | |||
| {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> | |||
| ); | |||
| } | |||
| @@ -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 NodeStatus } from '@/types'; | |||
| import { AutoMLTaskType } from '@/enums'; | |||
| import { HyperParameterFile } from '@/pages/HyperParameter/types'; | |||
| // 操作类型 | |||
| export enum OperationType { | |||
| @@ -82,4 +75,18 @@ export type ActiveLearnInstanceData = { | |||
| update_time: string; | |||
| finish_time: string; | |||
| 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 | |||
| checked={isSingleChecked(item.id)} | |||
| onChange={() => checkSingle(item.id)} | |||
| disabled={ | |||
| item.status === ExperimentStatus.Running || item.status === ExperimentStatus.Pending | |||
| } | |||
| ></Checkbox> | |||
| </div> | |||
| <a | |||
| @@ -357,7 +357,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| return ( | |||
| <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__filter']}> | |||
| <Input.Search | |||
| @@ -55,19 +55,4 @@ | |||
| .table-best-row { | |||
| color: @success-color; | |||
| 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 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 { downLoadZip } from '@/utils/downloadfile'; | |||
| import { to } from '@/utils/promise'; | |||
| 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 { useEffect, useState } from 'react'; | |||
| import TrialFileTree from '../TrialFileTree'; | |||
| import styles from './index.less'; | |||
| const { DirectoryTree } = Tree; | |||
| type ExperimentHistoryProps = { | |||
| trialList?: HyperParameterTrial[]; | |||
| }; | |||
| @@ -110,7 +106,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| if (metricNames.length) { | |||
| trialColumns.push({ | |||
| title: `指标分析(${first.metric ?? ''})`, | |||
| title: `指标分析(${first?.metric ?? ''})`, | |||
| dataIndex: 'metrics', | |||
| key: 'metrics', | |||
| align: 'center', | |||
| @@ -127,51 +123,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| // 自定义展开视图 | |||
| 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, | |||
| onExpand: handleExpandChange, | |||
| expandedRowKeys: expandedRowKeys, | |||
| rowExpandable: (record: HyperParameterTrial) => !!record.file, | |||
| }} | |||
| 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'; | |||
| /** 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 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 blob = new Blob([res.data], { type: mimeType }); | |||
| // 从response的headers中获取filename, | |||
| @@ -20,7 +21,7 @@ export function resolveBlob(res: any, mimeType: string) { | |||
| // console.log(res); | |||
| const contentDisposition = decodeURI(res.headers['content-disposition']); | |||
| const result = patt.exec(contentDisposition); | |||
| let fileName = result ? result[1] : 'file'; | |||
| let fileName = result ? result[1] : specifiedFileName; | |||
| fileName = fileName.replace(/"/g, ''); | |||
| aLink.style.display = 'none'; | |||
| aLink.href = URL.createObjectURL(blob); | |||
| @@ -43,7 +44,7 @@ export function downLoadZip(url: string, params?: any) { | |||
| responseType: 'blob', | |||
| getResponse: true, | |||
| }).then((res) => { | |||
| resolveBlob(res, mimeMap.zip); | |||
| resolveBlob(res, MimeType.ZIP); | |||
| }); | |||
| } | |||
| @@ -64,7 +65,30 @@ export async function downloadXlsx( | |||
| responseType: 'blob', | |||
| getResponse: true, | |||
| }).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); | |||
| }); | |||
| } | |||