Browse Source

feat: 完成实验对比

pull/94/head
cp3hnu 1 year ago
parent
commit
5b78d04abe
18 changed files with 357 additions and 175 deletions
  1. +11
    -11
      react-ui/config/routes.ts
  2. +37
    -0
      react-ui/src/components/ArrayTableCell/index.tsx
  3. +2
    -2
      react-ui/src/components/CommonTableCell/index.tsx
  4. +6
    -0
      react-ui/src/enums/index.ts
  5. +27
    -1
      react-ui/src/overrides.less
  6. +12
    -0
      react-ui/src/pages/Experiment/Comparison/index.less
  7. +140
    -155
      react-ui/src/pages/Experiment/Comparison/index.tsx
  8. +0
    -0
      react-ui/src/pages/Experiment/Info/index.jsx
  9. +0
    -0
      react-ui/src/pages/Experiment/Info/index.less
  10. +0
    -0
      react-ui/src/pages/Experiment/Info/props.less
  11. +0
    -0
      react-ui/src/pages/Experiment/Info/props.tsx
  12. +12
    -0
      react-ui/src/pages/Experiment/components/ExperimentStatusCell/index.less
  13. +28
    -0
      react-ui/src/pages/Experiment/components/ExperimentStatusCell/index.tsx
  14. +2
    -2
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  15. +1
    -1
      react-ui/src/pages/Experiment/components/LogList/index.tsx
  16. +2
    -2
      react-ui/src/pages/ModelDeployment/List/index.tsx
  17. +9
    -1
      react-ui/src/services/experiment/index.js
  18. +68
    -0
      react-ui/src/utils/table.tsx

+ 11
- 11
react-ui/config/routes.ts View File

@@ -94,7 +94,7 @@ export default [
{ {
name: '实验训练', name: '实验训练',
path: ':workflowId/:id', path: ':workflowId/:id',
component: './Experiment/training/index',
component: './Experiment/Info/index',
}, },
{ {
name: '实验对比', name: '实验对比',
@@ -112,18 +112,18 @@ export default [
{ {
name: '开发环境', name: '开发环境',
path: '', path: '',
component: './DevelopmentEnvironment/List',
},
{
name: '创建编辑器',
path: 'create',
component: './DevelopmentEnvironment/Create',
},
{
name: '编辑器',
path: 'editor',
component: './DevelopmentEnvironment/Editor', component: './DevelopmentEnvironment/Editor',
}, },
// {
// name: '创建编辑器',
// path: 'create',
// component: './DevelopmentEnvironment/Create',
// },
// {
// name: '编辑器',
// path: 'editor',
// component: './DevelopmentEnvironment/Editor',
// },
], ],
}, },
{ {


+ 37
- 0
react-ui/src/components/ArrayTableCell/index.tsx View File

@@ -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;

+ 2
- 2
react-ui/src/components/CommonTableCell/index.tsx View File

@@ -6,13 +6,13 @@


import { Tooltip } from 'antd'; import { Tooltip } from 'antd';


function renderCell(text?: string | null) {
function renderCell(text?: any | null) {
return <span>{text ?? '--'}</span>; return <span>{text ?? '--'}</span>;
} }


function CommonTableCell(ellipsis: boolean = false) { function CommonTableCell(ellipsis: boolean = false) {
if (ellipsis) { if (ellipsis) {
return (text?: string | null) => (
return (text?: any | null) => (
<Tooltip title={text} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}> <Tooltip title={text} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}>
{renderCell(text)} {renderCell(text)}
</Tooltip> </Tooltip>


+ 6
- 0
react-ui/src/enums/index.ts View File

@@ -1,3 +1,9 @@
/*
* @Author: 赵伟
* @Date: 2024-06-07 11:22:28
* @Description: 接口返回的枚举值和共用的枚举值定义在这里
*/

// 公开还是私有 TabKey // 公开还是私有 TabKey
export enum CommonTabKeys { export enum CommonTabKeys {
Private = 'Private', // 私有 Private = 'Private', // 私有


+ 27
- 1
react-ui/src/overrides.less View File

@@ -4,7 +4,7 @@
* @Description: 覆盖 antd 样式 * @Description: 覆盖 antd 样式
*/ */


// 设置 Table 可以滑动
// 设置 Table 可以滑动,带分页
.vertical-scroll-table { .vertical-scroll-table {
.ant-table-wrapper { .ant-table-wrapper {
height: 100%; 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 样式 // Tabs 样式
// 删除底部白色横线 // 删除底部白色横线
.ant-tabs { .ant-tabs {


+ 12
- 0
react-ui/src/pages/Experiment/Comparison/index.less View File

@@ -17,5 +17,17 @@
padding: 20px 30px 0; padding: 20px 30px 0;
background-color: white; background-color: white;
border-radius: 10px; border-radius: 10px;

:global {
.ant-table-container {
border: none !important;
}
.ant-table-tbody {
.ant-table-cell {
border-right: none !important;
border-left: none !important;
}
}
}
} }
} }

+ 140
- 155
react-ui/src/pages/Experiment/Comparison/index.tsx View File

@@ -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 { to } from '@/utils/promise';
import tableCellRender, { arrayFormatter, dateFormatter } from '@/utils/table';
import { useSearchParams } from '@umijs/max'; 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 classNames from 'classnames';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import ExperimentStatusCell from '../components/ExperimentStatusCell';
import styles from './index.less'; import styles from './index.less';


export enum ComparisonType { export enum ComparisonType {
@@ -13,20 +18,33 @@ export enum ComparisonType {
Evaluate = 'Evaluate', // 评估 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() { function ExperimentComparison() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const comparisonType = searchParams.get('type'); 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 [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(() => { useEffect(() => {
getComparisonData(); getComparisonData();
@@ -38,20 +56,39 @@ function ExperimentComparison() {
comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq;
const [res] = await to(request(experimentId)); const [res] = await to(request(experimentId));
if (res && res.data) { 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'] = { const rowSelection: TableProps['rowSelection'] = {
type: 'checkbox', type: 'checkbox',
selectedRowKeys, 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 ( 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">可视化对比</Button>
<Button type="default" onClick={hanldeComparisonClick}>
可视化对比
</Button>
</div> </div>
<div className={classNames('vertical-scroll-table', styles['experiment-comparison__table'])}>
<div
className={classNames(
'vertical-scroll-table-no-page',
styles['experiment-comparison__table'],
)}
>
<Table <Table
dataSource={tableData} dataSource={tableData}
columns={columns} columns={columns}
rowSelection={rowSelection} rowSelection={rowSelection}
scroll={{ y: 'calc(100% - 55px)' }} 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>
</div> </div>


react-ui/src/pages/Experiment/training/index.jsx → react-ui/src/pages/Experiment/Info/index.jsx View File


react-ui/src/pages/Experiment/training/index.less → react-ui/src/pages/Experiment/Info/index.less View File


react-ui/src/pages/Experiment/training/props.less → react-ui/src/pages/Experiment/Info/props.less View File


react-ui/src/pages/Experiment/training/props.tsx → react-ui/src/pages/Experiment/Info/props.tsx View File


+ 12
- 0
react-ui/src/pages/Experiment/components/ExperimentStatusCell/index.less View File

@@ -0,0 +1,12 @@
.experiment-status-cell {
height: 100%;
&__label {
display: none;
}
}

.experiment-status-cell:hover {
.experiment-status-cell__label {
display: inline;
}
}

+ 28
- 0
react-ui/src/pages/Experiment/components/ExperimentStatusCell/index.tsx View File

@@ -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;

+ 2
- 2
react-ui/src/pages/Experiment/components/LogGroup/index.tsx View File

@@ -6,7 +6,7 @@


import { ExperimentStatus } from '@/enums'; import { ExperimentStatus } from '@/enums';
import { useStateRef } from '@/hooks'; 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 { getExperimentPodsLog } from '@/services/experiment/index.js';
import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons'; import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons';
import { Button } from 'antd'; import { Button } from 'antd';
@@ -47,7 +47,7 @@ function LogGroup({
const [logList, setLogList, logListRef] = useStateRef<Log[]>([]); const [logList, setLogList, logListRef] = useStateRef<Log[]>([]);
const [completed, setCompleted] = useState(false); const [completed, setCompleted] = useState(false);
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false);
const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false);


useEffect(() => { useEffect(() => {
scrollToBottom(false); scrollToBottom(false);


+ 1
- 1
react-ui/src/pages/Experiment/components/LogList/index.tsx View File

@@ -1,5 +1,5 @@
import { ExperimentStatus } from '@/enums'; import { ExperimentStatus } from '@/enums';
import { ExperimentLog } from '@/pages/Experiment/training/props';
import { ExperimentLog } from '@/pages/Experiment/Info/props';
import LogGroup from '../LogGroup'; import LogGroup from '../LogGroup';
import styles from './index.less'; import styles from './index.less';




+ 2
- 2
react-ui/src/pages/ModelDeployment/List/index.tsx View File

@@ -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') { if (action === 'paginate') {
setPagination(pagination); setPagination(pagination);
} }
@@ -179,7 +179,7 @@ function ModelDeployment() {
dataIndex: 'index', dataIndex: 'index',
key: 'index', key: 'index',
width: '20%', width: '20%',
render(text, record, index) {
render(_text, _record, index) {
return <span>{(pagination.current! - 1) * pagination.pageSize! + index + 1}</span>; return <span>{(pagination.current! - 1) * pagination.pageSize! + index + 1}</span>;
}, },
}, },


+ 9
- 1
react-ui/src/services/experiment/index.js View File

@@ -126,7 +126,15 @@ export function getExpEvaluateInfosReq(experimentId) {


// 获取当前实验的模型训练指标信息 // 获取当前实验的模型训练指标信息
export function getExpTrainInfosReq(experimentId) { export function getExpTrainInfosReq(experimentId) {
return request(`/api/mmp//aim/getExpTrainInfos/${experimentId}`, {
return request(`/api/mmp/aim/getExpTrainInfos/${experimentId}`, {
method: 'GET', method: 'GET',
}); });
} }

// 获取当前实验的指标对比地址
export function getExpMetricsReq(data) {
return request(`/api/mmp/aim/getExpMetrics`, {
method: 'POST',
data
});
}

+ 68
- 0
react-ui/src/utils/table.tsx View File

@@ -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;

Loading…
Cancel
Save