| @@ -94,7 +94,7 @@ export default [ | |||
| { | |||
| name: '实验训练', | |||
| path: ':workflowId/:id', | |||
| component: './Experiment/training/index', | |||
| component: './Experiment/Info/index', | |||
| }, | |||
| { | |||
| name: '实验对比', | |||
| @@ -112,18 +112,18 @@ export default [ | |||
| { | |||
| name: '开发环境', | |||
| path: '', | |||
| component: './DevelopmentEnvironment/List', | |||
| }, | |||
| { | |||
| name: '创建编辑器', | |||
| path: 'create', | |||
| component: './DevelopmentEnvironment/Create', | |||
| }, | |||
| { | |||
| name: '编辑器', | |||
| path: 'editor', | |||
| component: './DevelopmentEnvironment/Editor', | |||
| }, | |||
| // { | |||
| // name: '创建编辑器', | |||
| // path: 'create', | |||
| // component: './DevelopmentEnvironment/Create', | |||
| // }, | |||
| // { | |||
| // name: '编辑器', | |||
| // path: 'editor', | |||
| // component: './DevelopmentEnvironment/Editor', | |||
| // }, | |||
| ], | |||
| }, | |||
| { | |||
| @@ -0,0 +1,37 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-28 14:18:11 | |||
| * @Description: 自定义 Table 数组类单元格 | |||
| */ | |||
| import { Tooltip } from 'antd'; | |||
| function ArrayTableCell(ellipsis: boolean = false, property?: string) { | |||
| return (value?: any | null) => { | |||
| if ( | |||
| value === undefined || | |||
| value === null || | |||
| Array.isArray(value) === false || | |||
| value.length === 0 | |||
| ) { | |||
| return <span>--</span>; | |||
| } | |||
| let list = value; | |||
| if (property && typeof value[0] === 'object') { | |||
| list = value.map((item) => item[property]); | |||
| } | |||
| const text = list.join(','); | |||
| if (ellipsis) { | |||
| return ( | |||
| <Tooltip title={text} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}> | |||
| <span>{text}</span>; | |||
| </Tooltip> | |||
| ); | |||
| } else { | |||
| return <span>{text}</span>; | |||
| } | |||
| }; | |||
| } | |||
| export default ArrayTableCell; | |||
| @@ -6,13 +6,13 @@ | |||
| import { Tooltip } from 'antd'; | |||
| function renderCell(text?: string | null) { | |||
| function renderCell(text?: any | null) { | |||
| return <span>{text ?? '--'}</span>; | |||
| } | |||
| function CommonTableCell(ellipsis: boolean = false) { | |||
| if (ellipsis) { | |||
| return (text?: string | null) => ( | |||
| return (text?: any | null) => ( | |||
| <Tooltip title={text} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}> | |||
| {renderCell(text)} | |||
| </Tooltip> | |||
| @@ -1,3 +1,9 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-06-07 11:22:28 | |||
| * @Description: 接口返回的枚举值和共用的枚举值定义在这里 | |||
| */ | |||
| // 公开还是私有 TabKey | |||
| export enum CommonTabKeys { | |||
| Private = 'Private', // 私有 | |||
| @@ -4,7 +4,7 @@ | |||
| * @Description: 覆盖 antd 样式 | |||
| */ | |||
| // 设置 Table 可以滑动 | |||
| // 设置 Table 可以滑动,带分页 | |||
| .vertical-scroll-table { | |||
| .ant-table-wrapper { | |||
| height: 100%; | |||
| @@ -30,6 +30,32 @@ | |||
| } | |||
| } | |||
| // 设置 Table 可以滑动,没有分页 | |||
| .vertical-scroll-table-no-page { | |||
| .ant-table-wrapper { | |||
| height: 100%; | |||
| .ant-spin-nested-loading { | |||
| height: 100%; | |||
| .ant-spin-container { | |||
| height: 100%; | |||
| .ant-table { | |||
| height: 100%; | |||
| .ant-table-container { | |||
| height: 100%; | |||
| .ant-table-body { | |||
| overflow-y: auto !important; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // Tabs 样式 | |||
| // 删除底部白色横线 | |||
| .ant-tabs { | |||
| @@ -17,5 +17,17 @@ | |||
| padding: 20px 30px 0; | |||
| background-color: white; | |||
| border-radius: 10px; | |||
| :global { | |||
| .ant-table-container { | |||
| border: none !important; | |||
| } | |||
| .ant-table-tbody { | |||
| .ant-table-cell { | |||
| border-right: none !important; | |||
| border-left: none !important; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,11 +1,16 @@ | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { getExpEvaluateInfosReq, getExpTrainInfosReq } from '@/services/experiment'; | |||
| // import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { | |||
| getExpEvaluateInfosReq, | |||
| getExpMetricsReq, | |||
| getExpTrainInfosReq, | |||
| } from '@/services/experiment'; | |||
| import { to } from '@/utils/promise'; | |||
| import tableCellRender, { arrayFormatter, dateFormatter } from '@/utils/table'; | |||
| import { useSearchParams } from '@umijs/max'; | |||
| import { Button, Table, TablePaginationConfig, TableProps } from 'antd'; | |||
| import { App, Button, Table, /*TablePaginationConfig,*/ TableProps } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useEffect, useMemo, useState } from 'react'; | |||
| import ExperimentStatusCell from '../components/ExperimentStatusCell'; | |||
| import styles from './index.less'; | |||
| export enum ComparisonType { | |||
| @@ -13,20 +18,33 @@ export enum ComparisonType { | |||
| Evaluate = 'Evaluate', // 评估 | |||
| } | |||
| type TableData = { | |||
| experiment_ins_id: number; | |||
| run_id: string; | |||
| dataset: string[]; | |||
| start_time: string; | |||
| status: string; | |||
| metrics_names: string[]; | |||
| metrics: Record<string, number>; | |||
| params_names: string[]; | |||
| params: Record<string, string>; | |||
| }; | |||
| function ExperimentComparison() { | |||
| const [searchParams] = useSearchParams(); | |||
| const comparisonType = searchParams.get('type'); | |||
| const experimentId = searchParams.get('experimentId'); | |||
| const [tableData, setTableData] = useState([]); | |||
| const [cacheState, setCacheState] = useCacheState(); | |||
| const [total, setTotal] = useState(0); | |||
| const experimentId = searchParams.get('id'); | |||
| const [tableData, setTableData] = useState<TableData[]>([]); | |||
| // const [cacheState, setCacheState] = useCacheState(); | |||
| // const [total, setTotal] = useState(0); | |||
| const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); | |||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||
| cacheState?.pagination ?? { | |||
| current: 1, | |||
| pageSize: 10, | |||
| }, | |||
| ); | |||
| const { message } = App.useApp(); | |||
| // const [pagination, setPagination] = useState<TablePaginationConfig>( | |||
| // cacheState?.pagination ?? { | |||
| // current: 1, | |||
| // pageSize: 10, | |||
| // }, | |||
| // ); | |||
| useEffect(() => { | |||
| getComparisonData(); | |||
| @@ -38,20 +56,39 @@ function ExperimentComparison() { | |||
| comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; | |||
| const [res] = await to(request(experimentId)); | |||
| if (res && res.data) { | |||
| const { content = [], totalElements = 0 } = res.data; | |||
| setTableData(content); | |||
| setTotal(totalElements); | |||
| // const { content = [], totalElements = 0 } = res.data; | |||
| setTableData(res.data); | |||
| // setTotal(totalElements); | |||
| } | |||
| }; | |||
| // 分页切换 | |||
| const handleTableChange: TableProps['onChange'] = (pagination, filters, sorter, { action }) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| // 获取对比 url | |||
| const getExpMetrics = async () => { | |||
| const [res] = await to(getExpMetricsReq(selectedRowKeys)); | |||
| if (res && res.data) { | |||
| const url = res.data; | |||
| window.open(url, '_blank'); | |||
| } | |||
| // console.log(pagination, filters, sorter, action); | |||
| }; | |||
| // 对比按钮 click | |||
| const hanldeComparisonClick = () => { | |||
| if (selectedRowKeys.length < 2) { | |||
| message.error('请至少选择两项进行对比'); | |||
| return; | |||
| } | |||
| getExpMetrics(); | |||
| }; | |||
| // 分页切换 | |||
| // const handleTableChange: TableProps['onChange'] = (pagination, filters, sorter, { action }) => { | |||
| // if (action === 'paginate') { | |||
| // setPagination(pagination); | |||
| // } | |||
| // // console.log(pagination, filters, sorter, action); | |||
| // }; | |||
| // 选择行 | |||
| const rowSelection: TableProps['rowSelection'] = { | |||
| type: 'checkbox', | |||
| selectedRowKeys, | |||
| @@ -61,148 +98,96 @@ function ExperimentComparison() { | |||
| }, | |||
| }; | |||
| const columns: TableProps['columns'] = [ | |||
| { | |||
| title: '基本信息', | |||
| children: [ | |||
| { | |||
| title: '实例ID', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| }, | |||
| { | |||
| title: '运行时间', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| }, | |||
| { | |||
| title: '运行状态', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| }, | |||
| { | |||
| title: '训练数据集', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| }, | |||
| { | |||
| title: '增量训练', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| title: '训练参数', | |||
| children: [ | |||
| { | |||
| title: 'batchsize', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| }, | |||
| { | |||
| title: 'config', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| }, | |||
| { | |||
| title: 'epoch', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| }, | |||
| { | |||
| title: 'lr', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| }, | |||
| { | |||
| title: 'warmup_iters', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| title: '训练指标', | |||
| children: [ | |||
| { | |||
| title: 'metrc_name', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| }, | |||
| { | |||
| title: 'test_1', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| }, | |||
| { | |||
| title: 'test_2', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| }, | |||
| { | |||
| title: 'test_3', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| }, | |||
| { | |||
| title: 'test_4', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| }, | |||
| ], | |||
| }, | |||
| ]; | |||
| const columns: TableProps['columns'] = useMemo(() => { | |||
| const first: TableData | undefined = tableData[0]; | |||
| return [ | |||
| { | |||
| title: '基本信息', | |||
| children: [ | |||
| { | |||
| title: '实例 ID', | |||
| dataIndex: 'experiment_ins_id', | |||
| key: 'experiment_ins_id', | |||
| width: '20%', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '运行时间', | |||
| dataIndex: 'start_time', | |||
| key: 'start_time', | |||
| width: 180, | |||
| render: tableCellRender(false, dateFormatter), | |||
| }, | |||
| { | |||
| title: '运行状态', | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: '20%', | |||
| render: ExperimentStatusCell, | |||
| }, | |||
| { | |||
| title: '训练数据集', | |||
| dataIndex: 'dataset', | |||
| key: 'dataset', | |||
| width: '20%', | |||
| render: tableCellRender(true, arrayFormatter()), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| title: '训练参数', | |||
| children: first?.params_names.map((name) => ({ | |||
| title: name, | |||
| dataIndex: ['params', name], | |||
| key: name, | |||
| width: '20%', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| })), | |||
| }, | |||
| { | |||
| title: '训练指标', | |||
| children: first?.metrics_names.map((name) => ({ | |||
| title: name, | |||
| dataIndex: ['metrics', name], | |||
| key: name, | |||
| width: '20%', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| })), | |||
| }, | |||
| ]; | |||
| }, [tableData]); | |||
| return ( | |||
| <div className={styles['experiment-comparison']}> | |||
| <div className={styles['experiment-comparison__header']}> | |||
| <Button type="default">可视化对比</Button> | |||
| <Button type="default" onClick={hanldeComparisonClick}> | |||
| 可视化对比 | |||
| </Button> | |||
| </div> | |||
| <div className={classNames('vertical-scroll-table', styles['experiment-comparison__table'])}> | |||
| <div | |||
| className={classNames( | |||
| 'vertical-scroll-table-no-page', | |||
| styles['experiment-comparison__table'], | |||
| )} | |||
| > | |||
| <Table | |||
| dataSource={tableData} | |||
| columns={columns} | |||
| rowSelection={rowSelection} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| pagination={{ | |||
| ...pagination, | |||
| total: total, | |||
| showSizeChanger: true, | |||
| showQuickJumper: true, | |||
| }} | |||
| onChange={handleTableChange} | |||
| rowKey="id" | |||
| pagination={false} | |||
| bordered={true} | |||
| // pagination={{ | |||
| // ...pagination, | |||
| // total: total, | |||
| // showSizeChanger: true, | |||
| // showQuickJumper: true, | |||
| // }} | |||
| // onChange={handleTableChange} | |||
| rowKey="run_id" | |||
| /> | |||
| </div> | |||
| </div> | |||
| @@ -0,0 +1,12 @@ | |||
| .experiment-status-cell { | |||
| height: 100%; | |||
| &__label { | |||
| display: none; | |||
| } | |||
| } | |||
| .experiment-status-cell:hover { | |||
| .experiment-status-cell__label { | |||
| display: inline; | |||
| } | |||
| } | |||
| @@ -0,0 +1,28 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-18 18:35:41 | |||
| * @Description: 实验状态 | |||
| */ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { experimentStatusInfo as statusInfo } from '@/pages/Experiment/status'; | |||
| import styles from './index.less'; | |||
| function ExperimentStatusCell(status?: ExperimentStatus | null) { | |||
| if (status === null || status === undefined || !statusInfo[status]) { | |||
| return <span>--</span>; | |||
| } | |||
| return ( | |||
| <div className={styles['experiment-status-cell']}> | |||
| <img style={{ width: '17px', marginRight: '7px' }} src={statusInfo[status]?.icon} /> | |||
| <span | |||
| style={{ color: statusInfo[status]?.color }} | |||
| className={styles['experiment-status-cell__label']} | |||
| > | |||
| {statusInfo[status]?.label} | |||
| </span> | |||
| </div> | |||
| ); | |||
| } | |||
| export default ExperimentStatusCell; | |||
| @@ -6,7 +6,7 @@ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { useStateRef } from '@/hooks'; | |||
| import { ExperimentLog } from '@/pages/Experiment/training/props'; | |||
| import { ExperimentLog } from '@/pages/Experiment/Info/props'; | |||
| import { getExperimentPodsLog } from '@/services/experiment/index.js'; | |||
| import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons'; | |||
| import { Button } from 'antd'; | |||
| @@ -47,7 +47,7 @@ function LogGroup({ | |||
| const [logList, setLogList, logListRef] = useStateRef<Log[]>([]); | |||
| const [completed, setCompleted] = useState(false); | |||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||
| const [isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | |||
| const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | |||
| useEffect(() => { | |||
| scrollToBottom(false); | |||
| @@ -1,5 +1,5 @@ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { ExperimentLog } from '@/pages/Experiment/training/props'; | |||
| import { ExperimentLog } from '@/pages/Experiment/Info/props'; | |||
| import LogGroup from '../LogGroup'; | |||
| import styles from './index.less'; | |||
| @@ -166,7 +166,7 @@ function ModelDeployment() { | |||
| }; | |||
| // 分页切换 | |||
| const handleTableChange: TableProps['onChange'] = (pagination, filters, sorter, { action }) => { | |||
| const handleTableChange: TableProps['onChange'] = (pagination, _filters, _sorter, { action }) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| @@ -179,7 +179,7 @@ function ModelDeployment() { | |||
| dataIndex: 'index', | |||
| key: 'index', | |||
| width: '20%', | |||
| render(text, record, index) { | |||
| render(_text, _record, index) { | |||
| return <span>{(pagination.current! - 1) * pagination.pageSize! + index + 1}</span>; | |||
| }, | |||
| }, | |||
| @@ -126,7 +126,15 @@ export function getExpEvaluateInfosReq(experimentId) { | |||
| // 获取当前实验的模型训练指标信息 | |||
| export function getExpTrainInfosReq(experimentId) { | |||
| return request(`/api/mmp//aim/getExpTrainInfos/${experimentId}`, { | |||
| return request(`/api/mmp/aim/getExpTrainInfos/${experimentId}`, { | |||
| method: 'GET', | |||
| }); | |||
| } | |||
| // 获取当前实验的指标对比地址 | |||
| export function getExpMetricsReq(data) { | |||
| return request(`/api/mmp/aim/getExpMetrics`, { | |||
| method: 'POST', | |||
| data | |||
| }); | |||
| } | |||
| @@ -0,0 +1,68 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-06-26 10:05:52 | |||
| * @Description: 列表自定义 render | |||
| */ | |||
| import { formatDate } from '@/utils/date'; | |||
| import { Tooltip } from 'antd'; | |||
| import dayjs from 'dayjs'; | |||
| type TableCellFormatter = (value?: any | null) => string | undefined | null; | |||
| // 字符串转换函数 | |||
| export const stringFormatter: TableCellFormatter = (value?: any | null) => { | |||
| return value; | |||
| }; | |||
| // 日期转换函数 | |||
| export const dateFormatter: TableCellFormatter = (value?: any | null) => { | |||
| if (value === undefined || value === null || value === '') { | |||
| return null; | |||
| } | |||
| if (!dayjs(value).isValid()) { | |||
| return null; | |||
| } | |||
| return formatDate(value); | |||
| }; | |||
| // 数组转换函数 | |||
| export function arrayFormatter(property?: string) { | |||
| return (value?: any | null): ReturnType<TableCellFormatter> => { | |||
| if ( | |||
| value === undefined || | |||
| value === null || | |||
| Array.isArray(value) === false || | |||
| value.length === 0 | |||
| ) { | |||
| return null; | |||
| } | |||
| let list = value; | |||
| if (property && typeof value[0] === 'object') { | |||
| list = value.map((item) => item[property]); | |||
| } | |||
| return list.join(','); | |||
| }; | |||
| } | |||
| function tableCellRender(ellipsis: boolean = false, format: TableCellFormatter = stringFormatter) { | |||
| return (value?: any | null) => { | |||
| const text = format(value); | |||
| if (ellipsis && text) { | |||
| return ( | |||
| <Tooltip title={text} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}> | |||
| {renderCell(text)} | |||
| </Tooltip> | |||
| ); | |||
| } else { | |||
| return renderCell(text); | |||
| } | |||
| }; | |||
| } | |||
| function renderCell(text?: any | null) { | |||
| return <span>{text ?? '--'}</span>; | |||
| } | |||
| export default tableCellRender; | |||