| @@ -245,6 +245,9 @@ export const antd: RuntimeAntdConfig = (memo) => { | |||||
| linkColor: 'rgba(29, 29, 32, 0.7)', | linkColor: 'rgba(29, 29, 32, 0.7)', | ||||
| separatorColor: '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.cssVar = true; | ||||
| // memo.theme.hashed = false; | // memo.theme.hashed = false; | ||||
| @@ -129,3 +129,23 @@ export const hyperParameterOptimizedModeOptions = [ | |||||
| { label: '越大越好', value: hyperParameterOptimizedMode.Max }, | { label: '越大越好', value: hyperParameterOptimizedMode.Max }, | ||||
| { label: '越小越好', value: hyperParameterOptimizedMode.Min }, | { 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, | key: TabKeys.History, | ||||
| label: 'Trial 列表', | |||||
| label: '试验列表', | |||||
| icon: <KFIcon type="icon-Trialliebiao" />, | icon: <KFIcon type="icon-Trialliebiao" />, | ||||
| children: ( | children: ( | ||||
| <ExperimentHistory | <ExperimentHistory | ||||
| @@ -4,6 +4,7 @@ import tableCellRender from '@/utils/table'; | |||||
| import { Table, type TableProps } from 'antd'; | import { Table, type TableProps } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||
| import TrialStatusCell from '../TrialStatusCell'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type ExperimentHistoryProps = { | type ExperimentHistoryProps = { | ||||
| @@ -103,7 +104,7 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps | |||||
| dataIndex: 'status', | dataIndex: 'status', | ||||
| key: 'status', | key: 'status', | ||||
| width: 120, | 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 | // 对比按钮 click | ||||
| const hanldeComparisonClick = () => { | |||||
| const handleComparisonClick = () => { | |||||
| if (selectedRowKeys.length < 2) { | if (selectedRowKeys.length < 2) { | ||||
| message.error('请至少选择两项进行对比'); | message.error('请至少选择两项进行对比'); | ||||
| return; | return; | ||||
| @@ -202,7 +202,7 @@ function ExperimentComparison() { | |||||
| return ( | return ( | ||||
| <div className={styles['experiment-comparison']}> | <div className={styles['experiment-comparison']}> | ||||
| <div className={styles['experiment-comparison__header']}> | <div className={styles['experiment-comparison__header']}> | ||||
| <Button type="default" onClick={hanldeComparisonClick}> | |||||
| <Button type="default" onClick={handleComparisonClick}> | |||||
| 可视化对比 | 可视化对比 | ||||
| </Button> | </Button> | ||||
| </div> | </div> | ||||
| @@ -191,7 +191,7 @@ function HyperParameterInstance() { | |||||
| }, | }, | ||||
| { | { | ||||
| key: TabKeys.History, | key: TabKeys.History, | ||||
| label: 'Trial 列表', | |||||
| label: '寻优列表', | |||||
| icon: <KFIcon type="icon-Trialliebiao" />, | icon: <KFIcon type="icon-Trialliebiao" />, | ||||
| children: <ExperimentHistory trialList={instanceInfo?.trial_list} />, | children: <ExperimentHistory trialList={instanceInfo?.trial_list} />, | ||||
| }, | }, | ||||
| @@ -8,7 +8,8 @@ | |||||
| border-radius: 10px; | border-radius: 10px; | ||||
| &__table { | &__table { | ||||
| height: 100%; | |||||
| height: calc(100% - 52px); | |||||
| margin-top: 20px; | |||||
| } | } | ||||
| :global { | :global { | ||||
| @@ -43,16 +44,31 @@ | |||||
| &__best-tag { | &__best-tag { | ||||
| margin-left: 8px; | margin-left: 8px; | ||||
| padding: 1px 10px; | padding: 1px 10px; | ||||
| color: @primary-color; | |||||
| color: @success-color; | |||||
| font-weight: normal; | font-weight: normal; | ||||
| font-size: 13px; | 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; | border-radius: 2px; | ||||
| } | } | ||||
| } | } | ||||
| .table-best-row { | .table-best-row { | ||||
| color: @primary-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,23 +1,33 @@ | |||||
| import InfoGroup from '@/components/InfoGroup'; | |||||
| import KFIcon from '@/components/KFIcon'; | 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 { downLoadZip } from '@/utils/downloadfile'; | ||||
| import { to } from '@/utils/promise'; | |||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | 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 classNames from 'classnames'; | ||||
| import { useState } from 'react'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| const { DirectoryTree } = Tree; | |||||
| type ExperimentHistoryProps = { | type ExperimentHistoryProps = { | ||||
| trialList?: HyperParameterTrialList[]; | |||||
| trialList?: HyperParameterTrial[]; | |||||
| }; | }; | ||||
| function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | 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 config: Record<string, any> = first?.config ?? {}; | ||||
| const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {}; | const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {}; | ||||
| const paramsNames = Object.keys(config); | const paramsNames = Object.keys(config); | ||||
| const metricNames = Object.keys(metricAnalysis); | const metricNames = Object.keys(metricAnalysis); | ||||
| const trialColumns: TableProps<HyperParameterTrialList>['columns'] = [ | |||||
| const trialColumns: TableProps<HyperParameterTrial>['columns'] = [ | |||||
| { | { | ||||
| title: '序号', | title: '序号', | ||||
| dataIndex: 'index', | dataIndex: 'index', | ||||
| @@ -55,7 +65,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||||
| dataIndex: 'status', | dataIndex: 'status', | ||||
| key: 'status', | key: 'status', | ||||
| width: 120, | 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: '文件名称', | title: '文件名称', | ||||
| dataIndex: 'name', | dataIndex: 'name', | ||||
| @@ -124,7 +134,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||||
| dataIndex: 'option', | dataIndex: 'option', | ||||
| width: 160, | width: 160, | ||||
| key: 'option', | key: 'option', | ||||
| render: (_: any, record: HyperParameterFileList) => { | |||||
| render: (_: any, record: HyperParameterFile) => { | |||||
| return ( | return ( | ||||
| <Button | <Button | ||||
| type="link" | 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" /> | <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 ( | return ( | ||||
| <div className={styles['experiment-history']}> | <div className={styles['experiment-history']}> | ||||
| <div className={styles['experiment-history__content']}> | <div className={styles['experiment-history__content']}> | ||||
| <Button type="default" onClick={handleComparisonClick}> | |||||
| 可视化对比 | |||||
| </Button> | |||||
| <div | <div | ||||
| className={classNames( | className={classNames( | ||||
| 'vertical-scroll-table-no-page', | 'vertical-scroll-table-no-page', | ||||
| @@ -167,7 +256,8 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||||
| bordered={true} | bordered={true} | ||||
| scroll={{ y: 'calc(100% - 110px)', x: '100%' }} | scroll={{ y: 'calc(100% - 110px)', x: '100%' }} | ||||
| rowKey="trial_id" | rowKey="trial_id" | ||||
| expandable={{ expandedRowRender }} | |||||
| expandable={{ expandedRowRender: expandedRowRender2 }} | |||||
| rowSelection={rowSelection} | |||||
| /> | /> | ||||
| </div> | </div> | ||||
| </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; | update_time: string; | ||||
| finish_time: string; | finish_time: string; | ||||
| nodeStatus?: NodeStatus; // json之后的节点状态 | nodeStatus?: NodeStatus; // json之后的节点状态 | ||||
| trial_list?: HyperParameterTrialList[]; | |||||
| file_list?: HyperParameterFileList[]; | |||||
| trial_list?: HyperParameterTrial[]; | |||||
| file_list?: HyperParameterFile[]; | |||||
| }; | }; | ||||
| export type HyperParameterTrialList = { | |||||
| export type HyperParameterTrial = { | |||||
| trial_id?: string; | trial_id?: string; | ||||
| training_iteration?: number; | training_iteration?: number; | ||||
| time?: number; | time?: number; | ||||
| @@ -71,14 +71,14 @@ export type HyperParameterTrialList = { | |||||
| config?: Record<string, any>; | config?: Record<string, any>; | ||||
| metric_analysis?: Record<string, any>; | metric_analysis?: Record<string, any>; | ||||
| metric: string; | metric: string; | ||||
| file: HyperParameterFileList; | |||||
| file: HyperParameterFile; | |||||
| is_best?: boolean; | is_best?: boolean; | ||||
| }; | }; | ||||
| export type HyperParameterFileList = { | |||||
| export type HyperParameterFile = { | |||||
| name: string; | name: string; | ||||
| size: string; | size: string; | ||||
| url: string; | url: string; | ||||
| isFile: boolean; | 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 | |||||
| }); | |||||
| } | |||||