Browse Source

feat: 添加实验实例

pull/146/head
cp3hnu 1 year ago
parent
commit
f8ab5bfd45
6 changed files with 510 additions and 88 deletions
  1. +163
    -84
      react-ui/src/pages/AutoML/List/index.tsx
  2. +71
    -0
      react-ui/src/pages/AutoML/components/ExperimentInstance/index.less
  3. +229
    -0
      react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx
  4. +6
    -1
      react-ui/src/pages/AutoML/types.ts
  5. +1
    -1
      react-ui/src/pages/Experiment/index.jsx
  6. +40
    -2
      react-ui/src/services/autoML.js

+ 163
- 84
react-ui/src/pages/AutoML/List/index.tsx View File

@@ -5,9 +5,17 @@
*/
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import { ExperimentStatus } from '@/enums';
import { useCacheState } from '@/hooks/pageCacheState';
import { deleteAutoMLReq, getAutoMLListReq, runAutoMLReq } from '@/services/autoML';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import {
deleteAutoMLReq,
getAutoMLListReq,
getExperimentInsListReq,
runAutoMLReq,
} from '@/services/autoML';
import themes from '@/styles/theme.less';
import { type ExperimentInstance as ExperimentInstanceData } from '@/types';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import tableCellRender, { TableCellValueType } from '@/utils/table';
@@ -25,8 +33,7 @@ import {
import { type SearchProps } from 'antd/es/input';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import ExecuteScheduleCell from '../components/ExecuteScheduleCell';
import RunStatusCell from '../components/RunStatusCell';
import ExperimentInstance from '../components/ExperimentInstance';
import { AutoMLData } from '../types';
import styles from './index.less';

@@ -38,6 +45,9 @@ function AutoMLList() {
const [inputText, setInputText] = useState(cacheState?.searchText);
const [tableData, setTableData] = useState<AutoMLData[]>([]);
const [total, setTotal] = useState(0);
const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]);
const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]);
const [experimentInsTotal, setExperimentInsTotal] = useState(0);
const [pagination, setPagination] = useState<TablePaginationConfig>(
cacheState?.pagination ?? {
current: 1,
@@ -46,11 +56,11 @@ function AutoMLList() {
);

useEffect(() => {
getServiceList();
getAutoMLList();
}, [pagination, searchText]);

// 获取模型部署服务列表
const getServiceList = async () => {
// 获取自主机器学习列表
const getAutoMLList = async () => {
const params: Record<string, any> = {
page: pagination.current! - 1,
size: pagination.pageSize,
@@ -64,8 +74,13 @@ function AutoMLList() {
}
};

// 搜索
const onSearch: SearchProps['onSearch'] = (value) => {
setSearchText(value);
};

// 删除模型部署
const deleteService = async (record: AutoMLData) => {
const deleteAutoML = async (record: AutoMLData) => {
const [res] = await to(deleteAutoMLReq(record.id));
if (res) {
message.success('删除成功');
@@ -78,29 +93,24 @@ function AutoMLList() {
current: 1,
}));
} else {
getServiceList();
getAutoMLList();
}
}
};

// 搜索
const onSearch: SearchProps['onSearch'] = (value) => {
setSearchText(value);
};

// 处理删除
const handleAutoMLDelete = (record: AutoMLData) => {
modalConfirm({
title: '删除后,该实验将不可恢复',
content: '是否确认删除?',
onOk: () => {
deleteService(record);
deleteAutoML(record);
},
});
};

// 创建、编辑
const createService = (record?: AutoMLData, isCopy: boolean = false) => {
// 创建、编辑、复制自动机器学习
const createAutoML = (record?: AutoMLData, isCopy: boolean = false) => {
setCacheState({
pagination,
searchText,
@@ -119,8 +129,8 @@ function AutoMLList() {
}
};

// 查看详情
const toDetail = (record: AutoMLData) => {
// 查看自动机器学习详情
const gotoDetail = (record: AutoMLData) => {
setCacheState({
pagination,
searchText,
@@ -129,24 +139,90 @@ function AutoMLList() {
navigate(`/pipeline/autoML/info/${record.id}`);
};

// 启动
// 启动自动机器学习
const startAutoML = async (record: AutoMLData) => {
const [res] = await to(runAutoMLReq(record.id));
if (res) {
message.success('操作成功');
getServiceList();
getAutoMLList();
}
};

// 停止
const stopAutoML = async (record: AutoMLData) => {
const [res] = await to(runAutoMLReq(record.id));
if (res) {
message.success('操作成功');
getServiceList();
// --------------------------- 实验实例 ---------------------------
// 获取实验实例列表
const getExperimentInsList = async (autoMLId: number, page: number) => {
const params = {
autoMlId: autoMLId,
page: page,
size: 5,
};
const [res] = await to(getExperimentInsListReq(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
try {
if (page === 0) {
setExperimentInsList(content);
} else {
setExperimentInsList((prev) => [...prev, ...content]);
}
setExperimentInsTotal(totalElements);
} catch (error) {
console.error('JSON parse error: ', error);
}
}
};
// 展开实例
const handleExpandChange = (expanded: boolean, record: AutoMLData) => {
setExperimentInsList([]);
if (expanded) {
setExpandedRowKeys([record.id]);
getExperimentInsList(record.id, 0);
} else {
setExpandedRowKeys([]);
}
};

// 跳转到实验实例详情
const gotoInstanceInfo = (item, record) => {
navigate({ pathname: `/pipeline/experiment/instance/${record.workflow_id}/${item.id}` });
};

// 刷新实验实例列表
const refreshExperimentIns = (experimentId: number) => {
getExperimentInsList(experimentId, 0);
};

// 加载更多实验实例
const loadMoreExperimentIns = () => {
const page = Math.round(experimentInsList.length / 5);
const autoMLId = expandedRowKeys[0];
getExperimentInsList(autoMLId, page);
};

// 实验实例终止
const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => {
// 刷新实验列表
refreshExperimentList();
setExperimentInsList((prevList) => {
return prevList.map((item) => {
if (item.id === experimentIns.id) {
return {
...item,
status: ExperimentStatus.Terminated,
};
}
return item;
});
});
};

// 刷新实验列表状态,
// 目前是直接刷新实验列表,后续需要优化,只刷新状态
const refreshExperimentList = () => {
getAutoMLList();
};

// --------------------------- Table ---------------------------
// 分页切换
const handleTableChange: TableProps<AutoMLData>['onChange'] = (
pagination,
@@ -160,47 +236,23 @@ function AutoMLList() {
};

const columns: TableProps<AutoMLData>['columns'] = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 80,
render: tableCellRender(false, TableCellValueType.Index, {
page: pagination.current! - 1,
pageSize: pagination.pageSize!,
}),
},
{
title: '实验名称',
dataIndex: 'ml_name',
key: 'ml_name',
width: '20%',
width: '16%',
render: tableCellRender(false, TableCellValueType.Link, {
onClick: toDetail,
onClick: gotoDetail,
}),
},
{
title: '实验描述',
dataIndex: 'ml_description',
key: 'ml_description',
width: '20%',
render: tableCellRender(true),
ellipsis: { showTitle: false },
},
{
title: '状态',
dataIndex: 'run_state',
key: 'run_state',
width: 100,
render: RunStatusCell,
},
{
title: '实验实例执行进度',
dataIndex: 'progress',
key: 'progress',
render: ExecuteScheduleCell,
width: 180,
},

{
title: '创建时间',
dataIndex: 'update_time',
@@ -210,26 +262,53 @@ function AutoMLList() {
ellipsis: { showTitle: false },
},
{
title: '修改时间',
dataIndex: 'update_time',
key: 'update_time',
width: '20%',
render: tableCellRender(true, TableCellValueType.Date),
ellipsis: { showTitle: false },
title: '最近五次运行状态',
dataIndex: 'status_list',
key: 'status_list',
width: 200,
render: (text) => {
const newText: string[] = text && text.replace(/\s+/g, '').split(',');
return (
<>
{newText && newText.length > 0
? newText.map((item, index) => {
return (
<img
style={{ width: '17px', marginRight: '6px' }}
key={index}
src={experimentStatusInfo[item as ExperimentStatus].icon}
draggable={false}
alt=""
/>
);
})
: null}
</>
);
},
},
{
title: '操作',
dataIndex: 'operation',
width: 320,
width: 360,
key: 'operation',
render: (_: any, record: AutoMLData) => (
<div>
<Button
type="link"
size="small"
key="start"
icon={<KFIcon type="icon-yunhang" />}
onClick={() => startAutoML(record)}
>
运行
</Button>
<Button
type="link"
size="small"
key="edit"
icon={<KFIcon type="icon-bianji" />}
onClick={() => createService(record, false)}
onClick={() => createAutoML(record, false)}
>
编辑
</Button>
@@ -238,31 +317,11 @@ function AutoMLList() {
size="small"
key="copy"
icon={<KFIcon type="icon-fuzhi" />}
onClick={() => createService(record, true)}
onClick={() => createAutoML(record, true)}
>
复制
</Button>
{record.run_state === 'Running' ? (
<Button
type="link"
size="small"
key="stop"
icon={<KFIcon type="icon-tingzhi" />}
onClick={() => startAutoML(record)}
>
停止
</Button>
) : (
<Button
type="link"
size="small"
key="start"
icon={<KFIcon type="icon-yunhang" />}
onClick={() => stopAutoML(record)}
>
运行
</Button>
)}

<ConfigProvider
theme={{
token: {
@@ -301,7 +360,7 @@ function AutoMLList() {
<Button
style={{ marginLeft: '20px' }}
type="default"
onClick={() => createService()}
onClick={() => createAutoML()}
icon={<KFIcon type="icon-xinjian2" />}
>
新建实验
@@ -322,6 +381,26 @@ function AutoMLList() {
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
expandable={{
expandedRowRender: (record) => (
<ExperimentInstance
experimentInsList={experimentInsList}
experimentInsTotal={experimentInsTotal}
onClickInstance={(item) => gotoInstanceInfo(item, record)}
onRemove={() => {
refreshExperimentIns(record.id);
refreshExperimentList();
}}
onTerminate={handleInstanceTerminate}
onLoadMore={() => loadMoreExperimentIns()}
></ExperimentInstance>
),
onExpand: (e, a) => {
handleExpandChange(e, a);
},
expandedRowKeys: expandedRowKeys,
rowExpandable: () => true,
}}
rowKey="id"
/>
</div>


+ 71
- 0
react-ui/src/pages/AutoML/components/ExperimentInstance/index.less View File

@@ -0,0 +1,71 @@
.tableExpandBox {
display: flex;
align-items: center;
width: 100%;
padding: 0 0 0 33px;
color: @text-color;
font-size: 14px;

& > div {
padding: 0 16px;
}

.check {
width: calc((100% + 32px + 33px) / 6.25 / 2);
}

.index {
width: calc((100% + 32px + 33px) / 6.25 / 2);
}

.description {
display: flex;
flex: 1;
align-items: center;
}

.startTime {
.singleLine();
width: calc(20% + 10px);
}

.status {
width: 200px;
}

.operation {
position: relative;
width: 344px;
}
}

.tableExpandBoxContent {
height: 45px;
background-color: #fff;
border: 1px solid #eaeaea;

& + & {
border-top: none;
}

.statusBox {
display: flex;
align-items: center;
width: 200px;

.statusIcon {
visibility: hidden;
transition: all 0.2s;
}
}
.statusBox:hover .statusIcon {
visibility: visible;
}
}

.loadMoreBox {
display: flex;
align-items: center;
justify-content: center;
margin: 16px auto 0;
}

+ 229
- 0
react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx View File

@@ -0,0 +1,229 @@
import KFIcon from '@/components/KFIcon';
import { ExperimentStatus } from '@/enums';
import { useCheck } from '@/hooks';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import {
batchDeleteExperimentInsReq,
deleteExperimentInsReq,
stopExperimentInsReq,
} from '@/services/autoML';
import themes from '@/styles/theme.less';
import { type ExperimentInstance } from '@/types';
import { elapsedTime, formatDate } from '@/utils/date';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { DoubleRightOutlined } from '@ant-design/icons';
import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd';
import classNames from 'classnames';
import { useEffect, useMemo } from 'react';
import styles from './index.less';

type ExperimentInstanceProps = {
experimentInsList?: ExperimentInstance[];
experimentInsTotal: number;
onClickInstance?: (instance: ExperimentInstance) => void;
onRemove?: () => void;
onTerminate?: (instance: ExperimentInstance) => void;
onLoadMore?: () => void;
};

function ExperimentInstanceComponent({
experimentInsList,
experimentInsTotal,
onClickInstance,
onRemove,
onTerminate,
onLoadMore,
}: ExperimentInstanceProps) {
const { message } = App.useApp();
const allIntanceIds = useMemo(() => {
return experimentInsList?.map((item) => item.id) || [];
}, [experimentInsList]);
const [
selectedIns,
setSelectedIns,
checked,
indeterminate,
checkAll,
isSingleChecked,
checkSingle,
] = useCheck(allIntanceIds);

useEffect(() => {
// 关闭时清空
if (allIntanceIds.length === 0) {
setSelectedIns([]);
}
}, [experimentInsList]);

// 删除实验实例确认
const handleRemove = (instance: ExperimentInstance) => {
modalConfirm({
title: '确定删除该条实例吗?',
onOk: () => {
deleteExperimentInstance(instance.id);
},
});
};

// 删除实验实例
const deleteExperimentInstance = async (id: number) => {
const [res] = await to(deleteExperimentInsReq(id));
if (res) {
message.success('删除成功');
onRemove?.();
}
};

// 批量删除实验实例确认
const handleDeleteAll = () => {
modalConfirm({
title: '确定批量删除选中的实例吗?',
onOk: () => {
batchDeleteExperimentInstances();
},
});
};

// 批量删除实验实例
const batchDeleteExperimentInstances = async () => {
const [res] = await to(batchDeleteExperimentInsReq(selectedIns));
if (res) {
message.success('删除成功');
setSelectedIns([]);
onRemove?.();
}
};

// 终止实验实例
const terminateExperimentInstance = async (instance: ExperimentInstance) => {
const [res] = await to(stopExperimentInsReq(instance.id));
if (res) {
message.success('终止成功');
onTerminate?.(instance);
}
};

if (!experimentInsList || experimentInsList.length === 0) {
return null;
}

return (
<div>
<div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}>
<div className={styles.check}>
<Checkbox checked={checked} indeterminate={indeterminate} onChange={checkAll}></Checkbox>
</div>
<div className={styles.index}>序号</div>
<div className={styles.description}>运行时长</div>
<div className={styles.startTime}>开始时间</div>
<div className={styles.status}>状态</div>
<div className={styles.operation}>
<span>操作</span>
{selectedIns.length > 0 && (
<Button
style={{ position: 'absolute', right: '0' }}
color="primary"
variant="filled"
size="small"
onClick={handleDeleteAll}
icon={<KFIcon type="icon-shanchu" />}
>
删除
</Button>
)}
</div>
</div>

{experimentInsList.map((item, index) => (
<div
key={item.id}
className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)}
>
<div className={styles.check}>
<Checkbox
checked={isSingleChecked(item.id)}
onChange={() => checkSingle(item.id)}
></Checkbox>
</div>
<a
className={styles.index}
style={{ padding: '0 16px' }}
onClick={() => onClickInstance?.(item)}
>
{index + 1}
</a>
<div className={styles.description}>
{elapsedTime(item.create_time, item.finish_time)}
</div>
<div className={styles.startTime}>
<Tooltip title={formatDate(item.create_time)}>
<span>{formatDate(item.create_time)}</span>
</Tooltip>
</div>
<div className={styles.statusBox}>
<img
style={{ width: '17px', marginRight: '7px' }}
src={experimentStatusInfo[item.status as ExperimentStatus]?.icon}
draggable={false}
alt=""
/>
<span
style={{ color: experimentStatusInfo[item.status as ExperimentStatus]?.color }}
className={styles.statusIcon}
>
{experimentStatusInfo[item.status as ExperimentStatus]?.label}
</span>
</div>
<div className={styles.operation}>
<Button
type="link"
size="small"
key="stop"
disabled={
item.status === ExperimentStatus.Succeeded ||
item.status === ExperimentStatus.Failed ||
item.status === ExperimentStatus.Terminated
}
icon={<KFIcon type="icon-zhongzhi" />}
onClick={() => terminateExperimentInstance(item)}
>
终止
</Button>
<ConfigProvider
theme={{
token: {
colorLink: themes['warningColor'],
},
}}
>
<Button
type="link"
size="small"
key="batchRemove"
disabled={
item.status === ExperimentStatus.Running ||
item.status === ExperimentStatus.Pending
}
icon={<KFIcon type="icon-shanchu" />}
onClick={() => handleRemove(item)}
>
删除
</Button>
</ConfigProvider>
</div>
</div>
))}
{experimentInsTotal > experimentInsList.length ? (
<div className={styles.loadMoreBox}>
<Button type="link" onClick={onLoadMore}>
更多
<DoubleRightOutlined rotate={90} />
</Button>
</div>
) : null}
</div>
);
}

export default ExperimentInstanceComponent;

+ 6
- 1
react-ui/src/pages/AutoML/types.ts View File

@@ -39,7 +39,7 @@ export type FormData = {
};

export type AutoMLData = {
id: string;
id: number;
progress: number;
run_state: string;
state: number;
@@ -55,7 +55,12 @@ export type AutoMLData = {
create_time?: string;
update_by?: string;
update_time?: string;
status_list: string; // 最近五次运行状态
} & Omit<
FormData,
'metrics|dataset|include_classifier|include_feature_preprocessor|include_regressor|exclude_classifier|exclude_feature_preprocessor|exclude_regressor'
>;

export type ExperimentInstanceData = {
id: number;
};

+ 1
- 1
react-ui/src/pages/Experiment/index.jsx View File

@@ -385,7 +385,7 @@ function Experiment() {
key: 'status_list',
width: 200,
render: (text) => {
let newText = text && text.replace(/\s+/g, '').split(',');
const newText = text && text.replace(/\s+/g, '').split(',');
return (
<>
{newText && newText.length > 0


+ 40
- 2
react-ui/src/services/autoML.js View File

@@ -48,8 +48,46 @@ export function deleteAutoMLReq(id) {

// 运行自动学习
export function runAutoMLReq(id) {
return request(`/api/mmp/autoML/${id}`, {
return request(`/api/mmp/autoML/run/${id}`, {
method: 'POST',
});
}

// ----------------------- 实验实例 -----------------------
// 获取实验实例列表
export function getExperimentInsListReq(params) {
return request(`/api/mmp/autoMLIns`, {
method: 'GET',
params,
});
}
}

// 查询实验实例详情
export function getExperimentInsReq(id) {
return request(`/api/mmp/autoMLIns/${id}`, {
method: 'GET',
});
}

// 停止实验实例
export function stopExperimentInsReq(id) {
return request(`/api/mmp/autoMLIns/${id}`, {
method: 'PUT',
});
}

// 删除实验实例
export function deleteExperimentInsReq(id) {
return request(`/api/mmp/autoMLIns/${id}`, {
method: 'DELETE',
});
}

// 批量删除实验实例
export function batchDeleteExperimentInsReq(data) {
return request(`/api/mmp/autoMLIns/batchDelete`, {
method: 'DELETE',
data
});
}


Loading…
Cancel
Save