| @@ -245,6 +245,9 @@ export const antd: RuntimeAntdConfig = (memo) => { | |||
| linkColor: 'rgba(29, 29, 32, 0.7)', | |||
| separatorColor: 'rgba(29, 29, 32, 0.7)', | |||
| }; | |||
| memo.theme.components.Tree = { | |||
| directoryNodeSelectedBg: 'rgba(22, 100, 255, 0.7)', | |||
| }; | |||
| memo.theme.cssVar = true; | |||
| // memo.theme.hashed = false; | |||
| @@ -129,3 +129,23 @@ export const hyperParameterOptimizedModeOptions = [ | |||
| { label: '越大越好', value: hyperParameterOptimizedMode.Max }, | |||
| { label: '越小越好', value: hyperParameterOptimizedMode.Min }, | |||
| ]; | |||
| // 超参数 Trail 运行状态 | |||
| export enum HyperParameterTrailStatus { | |||
| PENDING = 'PENDING', // 挂起 | |||
| RUNNING = 'RUNNING', // 运行中 | |||
| TERMINATED = 'TERMINATED', // 成功 | |||
| ERROR = 'ERROR', // 错误 | |||
| PAUSED = 'PAUSED', // 暂停 | |||
| RESTORING = 'RESTORING', // 恢复中 | |||
| } | |||
| // 自动 Trail 运行状态 | |||
| export enum AutoMLTrailStatus { | |||
| TIMEOUT = 'TIMEOUT', // 超时 | |||
| SUCCESS = 'SUCCESS', // 成功 | |||
| FAILURE = 'FAILURE', // 失败 | |||
| CRASHED = 'CRASHED', // 崩溃 | |||
| STOP = 'STOP', // 停止 | |||
| CANCELLED = 'CANCELLED', // 取消 | |||
| } | |||
| @@ -186,7 +186,7 @@ function AutoMLInstance() { | |||
| }, | |||
| { | |||
| key: TabKeys.History, | |||
| label: 'Trial 列表', | |||
| label: '试验列表', | |||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||
| children: ( | |||
| <ExperimentHistory | |||
| @@ -4,6 +4,7 @@ import tableCellRender from '@/utils/table'; | |||
| import { Table, type TableProps } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| import TrialStatusCell from '../TrialStatusCell'; | |||
| import styles from './index.less'; | |||
| type ExperimentHistoryProps = { | |||
| @@ -103,7 +104,7 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: 120, | |||
| render: tableCellRender(false), | |||
| render: TrialStatusCell, | |||
| }, | |||
| ]; | |||
| @@ -0,0 +1,3 @@ | |||
| .trial-status-cell { | |||
| height: 100%; | |||
| } | |||
| @@ -0,0 +1,67 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-18 18:35:41 | |||
| * @Description: 实验状态 | |||
| */ | |||
| import { AutoMLTrailStatus } from '@/enums'; | |||
| import { ExperimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import themes from '@/styles/theme.less'; | |||
| import styles from './index.less'; | |||
| export const statusInfo: Record<AutoMLTrailStatus, ExperimentStatusInfo> = { | |||
| [AutoMLTrailStatus.SUCCESS]: { | |||
| label: '成功', | |||
| color: themes.successColor, | |||
| icon: '/assets/images/experiment-status/success-icon.png', | |||
| }, | |||
| [AutoMLTrailStatus.TIMEOUT]: { | |||
| label: '超时', | |||
| color: themes.pendingColor, | |||
| icon: '/assets/images/experiment-status/pending-icon.png', | |||
| }, | |||
| [AutoMLTrailStatus.FAILURE]: { | |||
| label: '失败', | |||
| color: themes.errorColor, | |||
| icon: '/assets/images/experiment-status/fail-icon.png', | |||
| }, | |||
| [AutoMLTrailStatus.CRASHED]: { | |||
| label: '崩溃', | |||
| color: themes.errorColor, | |||
| icon: '/assets/images/experiment-status/fail-icon.png', | |||
| }, | |||
| [AutoMLTrailStatus.CANCELLED]: { | |||
| label: '取消', | |||
| color: themes.abortColor, | |||
| icon: '/assets/images/experiment-status/omitted-icon.png', | |||
| }, | |||
| [AutoMLTrailStatus.STOP]: { | |||
| label: '停止', | |||
| color: themes.textColor, | |||
| icon: '/assets/images/experiment-status/omitted-icon.png', | |||
| }, | |||
| }; | |||
| function TrialStatusCell(status?: AutoMLTrailStatus | null) { | |||
| if (status === null || status === undefined) { | |||
| return <span>--</span>; | |||
| } | |||
| return ( | |||
| <div className={styles['trial-status-cell']}> | |||
| {/* <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={statusInfo[status]?.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> */} | |||
| <span | |||
| style={{ color: statusInfo[status] ? statusInfo[status].color : themes.textColor }} | |||
| className={styles['trial-status-cell__label']} | |||
| > | |||
| {statusInfo[status] ? statusInfo[status].label : status} | |||
| </span> | |||
| </div> | |||
| ); | |||
| } | |||
| export default TrialStatusCell; | |||
| @@ -77,7 +77,7 @@ function ExperimentComparison() { | |||
| }; | |||
| // 对比按钮 click | |||
| const hanldeComparisonClick = () => { | |||
| const handleComparisonClick = () => { | |||
| if (selectedRowKeys.length < 2) { | |||
| message.error('请至少选择两项进行对比'); | |||
| return; | |||
| @@ -202,7 +202,7 @@ function ExperimentComparison() { | |||
| return ( | |||
| <div className={styles['experiment-comparison']}> | |||
| <div className={styles['experiment-comparison__header']}> | |||
| <Button type="default" onClick={hanldeComparisonClick}> | |||
| <Button type="default" onClick={handleComparisonClick}> | |||
| 可视化对比 | |||
| </Button> | |||
| </div> | |||
| @@ -191,7 +191,7 @@ function HyperParameterInstance() { | |||
| }, | |||
| { | |||
| key: TabKeys.History, | |||
| label: 'Trial 列表', | |||
| label: '寻优列表', | |||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||
| children: <ExperimentHistory trialList={instanceInfo?.trial_list} />, | |||
| }, | |||
| @@ -8,7 +8,8 @@ | |||
| border-radius: 10px; | |||
| &__table { | |||
| height: 100%; | |||
| height: calc(100% - 52px); | |||
| margin-top: 20px; | |||
| } | |||
| :global { | |||
| @@ -43,16 +44,31 @@ | |||
| &__best-tag { | |||
| margin-left: 8px; | |||
| padding: 1px 10px; | |||
| color: @primary-color; | |||
| color: @success-color; | |||
| font-weight: normal; | |||
| font-size: 13px; | |||
| background-color: .addAlpha(@primary-color, 0.1) []; | |||
| border: 1px solid .addAlpha(@primary-color, 0.5) []; | |||
| background-color: .addAlpha(@success-color, 0.1) []; | |||
| // border: 1px solid .addAlpha(@success-color, 0.5) []; | |||
| border-radius: 2px; | |||
| } | |||
| } | |||
| .table-best-row { | |||
| color: @primary-color; | |||
| 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,23 +1,33 @@ | |||
| import InfoGroup from '@/components/InfoGroup'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { HyperParameterFileList, HyperParameterTrialList } from '@/pages/HyperParameter/types'; | |||
| import TrialStatusCell from '@/pages/HyperParameter/components/TrialStatusCell'; | |||
| import { HyperParameterFile, 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 { Button, Table, Tooltip, type TableProps } from 'antd'; | |||
| import { App, Button, Table, Tooltip, Tree, type TableProps, type TreeDataNode } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useState } from 'react'; | |||
| import styles from './index.less'; | |||
| const { DirectoryTree } = Tree; | |||
| type ExperimentHistoryProps = { | |||
| trialList?: HyperParameterTrialList[]; | |||
| trialList?: HyperParameterTrial[]; | |||
| }; | |||
| function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| const first: HyperParameterTrialList | undefined = trialList[0]; | |||
| const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); | |||
| const { message } = App.useApp(); | |||
| const first: HyperParameterTrial | undefined = trialList[0]; | |||
| const config: Record<string, any> = first?.config ?? {}; | |||
| const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {}; | |||
| const paramsNames = Object.keys(config); | |||
| const metricNames = Object.keys(metricAnalysis); | |||
| const trialColumns: TableProps<HyperParameterTrialList>['columns'] = [ | |||
| const trialColumns: TableProps<HyperParameterTrial>['columns'] = [ | |||
| { | |||
| title: '序号', | |||
| dataIndex: 'index', | |||
| @@ -55,7 +65,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: 120, | |||
| render: tableCellRender(false), | |||
| render: TrialStatusCell, | |||
| }, | |||
| ]; | |||
| @@ -105,7 +115,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| }); | |||
| } | |||
| const fileColumns: TableProps<HyperParameterFileList>['columns'] = [ | |||
| const fileColumns: TableProps<HyperParameterFile>['columns'] = [ | |||
| { | |||
| title: '文件名称', | |||
| dataIndex: 'name', | |||
| @@ -124,7 +134,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| dataIndex: 'option', | |||
| width: 160, | |||
| key: 'option', | |||
| render: (_: any, record: HyperParameterFileList) => { | |||
| render: (_: any, record: HyperParameterFile) => { | |||
| return ( | |||
| <Button | |||
| type="link" | |||
| @@ -146,13 +156,92 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| }, | |||
| ]; | |||
| const expandedRowRender = (record: HyperParameterTrialList) => ( | |||
| const expandedRowRender = (record: HyperParameterTrial) => ( | |||
| <Table columns={fileColumns} dataSource={[record.file]} pagination={false} rowKey="name" /> | |||
| ); | |||
| const expandedRowRender2 = (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> | |||
| ); | |||
| }; | |||
| // 选择行 | |||
| const rowSelection: TableProps<HyperParameterTrial>['rowSelection'] = { | |||
| type: 'checkbox', | |||
| columnWidth: 48, | |||
| fixed: 'left', | |||
| selectedRowKeys, | |||
| onChange: (selectedRowKeys: React.Key[]) => { | |||
| setSelectedRowKeys(selectedRowKeys); | |||
| }, | |||
| }; | |||
| const handleComparisonClick = () => { | |||
| if (selectedRowKeys.length < 1) { | |||
| message.error('请至少选择一项'); | |||
| return; | |||
| } | |||
| getExpMetrics(); | |||
| }; | |||
| // 获取对比 url | |||
| const getExpMetrics = async () => { | |||
| const [res] = await to(getExpMetricsReq(selectedRowKeys)); | |||
| if (res && res.data) { | |||
| const url = res.data; | |||
| window.open(url, '_blank'); | |||
| } | |||
| }; | |||
| return ( | |||
| <div className={styles['experiment-history']}> | |||
| <div className={styles['experiment-history__content']}> | |||
| <Button type="default" onClick={handleComparisonClick}> | |||
| 可视化对比 | |||
| </Button> | |||
| <div | |||
| className={classNames( | |||
| 'vertical-scroll-table-no-page', | |||
| @@ -167,7 +256,8 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| bordered={true} | |||
| scroll={{ y: 'calc(100% - 110px)', x: '100%' }} | |||
| rowKey="trial_id" | |||
| expandable={{ expandedRowRender }} | |||
| expandable={{ expandedRowRender: expandedRowRender2 }} | |||
| rowSelection={rowSelection} | |||
| /> | |||
| </div> | |||
| </div> | |||
| @@ -0,0 +1,3 @@ | |||
| .trial-status-cell { | |||
| height: 100%; | |||
| } | |||
| @@ -0,0 +1,67 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-18 18:35:41 | |||
| * @Description: 实验状态 | |||
| */ | |||
| import { HyperParameterTrailStatus } from '@/enums'; | |||
| import { ExperimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import themes from '@/styles/theme.less'; | |||
| import styles from './index.less'; | |||
| export const statusInfo: Record<HyperParameterTrailStatus, ExperimentStatusInfo> = { | |||
| [HyperParameterTrailStatus.RUNNING]: { | |||
| label: '运行中', | |||
| color: themes.primaryColor, | |||
| icon: '/assets/images/experiment-status/running-icon.png', | |||
| }, | |||
| [HyperParameterTrailStatus.TERMINATED]: { | |||
| label: '成功', | |||
| color: themes.successColor, | |||
| icon: '/assets/images/experiment-status/success-icon.png', | |||
| }, | |||
| [HyperParameterTrailStatus.PENDING]: { | |||
| label: '挂起', | |||
| color: themes.pendingColor, | |||
| icon: '/assets/images/experiment-status/pending-icon.png', | |||
| }, | |||
| [HyperParameterTrailStatus.ERROR]: { | |||
| label: '失败', | |||
| color: themes.errorColor, | |||
| icon: '/assets/images/experiment-status/fail-icon.png', | |||
| }, | |||
| [HyperParameterTrailStatus.PAUSED]: { | |||
| label: '暂停', | |||
| color: themes.abortColor, | |||
| icon: '/assets/images/experiment-status/omitted-icon.png', | |||
| }, | |||
| [HyperParameterTrailStatus.RESTORING]: { | |||
| label: '恢复中', | |||
| color: themes.textColor, | |||
| icon: '/assets/images/experiment-status/omitted-icon.png', | |||
| }, | |||
| }; | |||
| function TrialStatusCell(status?: HyperParameterTrailStatus | null) { | |||
| if (status === null || status === undefined) { | |||
| return <span>--</span>; | |||
| } | |||
| return ( | |||
| <div className={styles['trial-status-cell']}> | |||
| {/* <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={statusInfo[status]?.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> */} | |||
| <span | |||
| style={{ color: statusInfo[status] ? statusInfo[status].color : themes.textColor }} | |||
| className={styles['trial-status-cell__label']} | |||
| > | |||
| {statusInfo[status] ? statusInfo[status].label : status} | |||
| </span> | |||
| </div> | |||
| ); | |||
| } | |||
| export default TrialStatusCell; | |||
| @@ -59,11 +59,11 @@ export type HyperParameterInstanceData = { | |||
| update_time: string; | |||
| finish_time: string; | |||
| nodeStatus?: NodeStatus; // json之后的节点状态 | |||
| trial_list?: HyperParameterTrialList[]; | |||
| file_list?: HyperParameterFileList[]; | |||
| trial_list?: HyperParameterTrial[]; | |||
| file_list?: HyperParameterFile[]; | |||
| }; | |||
| export type HyperParameterTrialList = { | |||
| export type HyperParameterTrial = { | |||
| trial_id?: string; | |||
| training_iteration?: number; | |||
| time?: number; | |||
| @@ -71,14 +71,14 @@ export type HyperParameterTrialList = { | |||
| config?: Record<string, any>; | |||
| metric_analysis?: Record<string, any>; | |||
| metric: string; | |||
| file: HyperParameterFileList; | |||
| file: HyperParameterFile; | |||
| is_best?: boolean; | |||
| }; | |||
| export type HyperParameterFileList = { | |||
| export type HyperParameterFile = { | |||
| name: string; | |||
| size: string; | |||
| url: string; | |||
| isFile: boolean; | |||
| children?: HyperParameterFileList[]; | |||
| children: HyperParameterFile[]; | |||
| }; | |||
| @@ -91,3 +91,10 @@ export function batchDeleteRayInsReq(data) { | |||
| }); | |||
| } | |||
| // 获取当前实验的指标对比地址 | |||
| export function getExpMetricsReq(data) { | |||
| return request(`/api/mmp/rayIns/getExpMetrics`, { | |||
| method: 'POST', | |||
| data | |||
| }); | |||
| } | |||