diff --git a/react-ui/src/pages/AutoML/List/index.tsx b/react-ui/src/pages/AutoML/List/index.tsx index 34a5d395..288001ec 100644 --- a/react-ui/src/pages/AutoML/List/index.tsx +++ b/react-ui/src/pages/AutoML/List/index.tsx @@ -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([]); const [total, setTotal] = useState(0); + const [experimentInsList, setExperimentInsList] = useState([]); + const [expandedRowKeys, setExpandedRowKeys] = useState([]); + const [experimentInsTotal, setExperimentInsTotal] = useState(0); const [pagination, setPagination] = useState( cacheState?.pagination ?? { current: 1, @@ -46,11 +56,11 @@ function AutoMLList() { ); useEffect(() => { - getServiceList(); + getAutoMLList(); }, [pagination, searchText]); - // 获取模型部署服务列表 - const getServiceList = async () => { + // 获取自主机器学习列表 + const getAutoMLList = async () => { const params: Record = { 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['onChange'] = ( pagination, @@ -160,47 +236,23 @@ function AutoMLList() { }; const columns: TableProps['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 ( + + ); + }) + : null} + + ); + }, }, { title: '操作', dataIndex: 'operation', - width: 320, + width: 360, key: 'operation', render: (_: any, record: AutoMLData) => (
+ @@ -238,31 +317,11 @@ function AutoMLList() { size="small" key="copy" icon={} - onClick={() => createService(record, true)} + onClick={() => createAutoML(record, true)} > 复制 - {record.run_state === 'Running' ? ( - - ) : ( - - )} + createService()} + onClick={() => createAutoML()} icon={} > 新建实验 @@ -322,6 +381,26 @@ function AutoMLList() { showTotal: () => `共${total}条`, }} onChange={handleTableChange} + expandable={{ + expandedRowRender: (record) => ( + gotoInstanceInfo(item, record)} + onRemove={() => { + refreshExperimentIns(record.id); + refreshExperimentList(); + }} + onTerminate={handleInstanceTerminate} + onLoadMore={() => loadMoreExperimentIns()} + > + ), + onExpand: (e, a) => { + handleExpandChange(e, a); + }, + expandedRowKeys: expandedRowKeys, + rowExpandable: () => true, + }} rowKey="id" />
diff --git a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.less b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.less new file mode 100644 index 00000000..499b8424 --- /dev/null +++ b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.less @@ -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; +} diff --git a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx new file mode 100644 index 00000000..72fe3473 --- /dev/null +++ b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx @@ -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 ( +
+
+
+ +
+
序号
+
运行时长
+
开始时间
+
状态
+
+ 操作 + {selectedIns.length > 0 && ( + + )} +
+
+ + {experimentInsList.map((item, index) => ( +
+
+ checkSingle(item.id)} + > +
+ onClickInstance?.(item)} + > + {index + 1} + +
+ {elapsedTime(item.create_time, item.finish_time)} +
+
+ + {formatDate(item.create_time)} + +
+
+ + + {experimentStatusInfo[item.status as ExperimentStatus]?.label} + +
+
+ + + + +
+
+ ))} + {experimentInsTotal > experimentInsList.length ? ( +
+ +
+ ) : null} +
+ ); +} + +export default ExperimentInstanceComponent; diff --git a/react-ui/src/pages/AutoML/types.ts b/react-ui/src/pages/AutoML/types.ts index 0cc18479..3464e599 100644 --- a/react-ui/src/pages/AutoML/types.ts +++ b/react-ui/src/pages/AutoML/types.ts @@ -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; +}; diff --git a/react-ui/src/pages/Experiment/index.jsx b/react-ui/src/pages/Experiment/index.jsx index 76ffd4c6..95bc2953 100644 --- a/react-ui/src/pages/Experiment/index.jsx +++ b/react-ui/src/pages/Experiment/index.jsx @@ -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 diff --git a/react-ui/src/services/autoML.js b/react-ui/src/services/autoML.js index d9d4a9b5..a4e39a99 100644 --- a/react-ui/src/services/autoML.js +++ b/react-ui/src/services/autoML.js @@ -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, }); -} \ No newline at end of file +} + +// 查询实验实例详情 +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 + }); +} +