diff --git a/react-ui/src/components/ParameterInput/index.less b/react-ui/src/components/ParameterInput/index.less index 2d4f0489..6426985e 100644 --- a/react-ui/src/components/ParameterInput/index.less +++ b/react-ui/src/components/ParameterInput/index.less @@ -62,3 +62,7 @@ font-size: 12px; } } + +.parameter-input.parameter-input--error { + border-color: @error-color; +} diff --git a/react-ui/src/components/ParameterInput/index.tsx b/react-ui/src/components/ParameterInput/index.tsx index 0c422bff..8ce18830 100644 --- a/react-ui/src/components/ParameterInput/index.tsx +++ b/react-ui/src/components/ParameterInput/index.tsx @@ -1,5 +1,5 @@ import { CloseOutlined } from '@ant-design/icons'; -import { Input } from 'antd'; +import { Form, Input } from 'antd'; import { RuleObject } from 'antd/es/form'; import classNames from 'classnames'; import './index.less'; @@ -31,6 +31,7 @@ export interface ParameterInputProps { style?: React.CSSProperties; size?: 'middle' | 'small' | 'large'; disabled?: boolean; + id?: string; } function ParameterInput({ @@ -45,6 +46,7 @@ function ParameterInput({ style, size = 'middle', disabled = false, + id, ...rest }: ParameterInputProps) { const valueObj = @@ -55,6 +57,7 @@ function ParameterInput({ const isSelect = valueObj?.fromSelect; const placeholder = valueObj?.placeholder || rest?.placeholder; const InputComponent = textArea ? Input.TextArea : Input; + const { status } = Form.Item.useStatus(); // 删除 const handleRemove = (e: React.MouseEvent) => { @@ -75,9 +78,11 @@ function ParameterInput({ <> {(isSelect || !canInput) && !disabled ? (
+
+ + + +
+
+ ))} + {experimentInsTotal > experimentInList.length ? ( +
+ +
+ ) : null} + + ); +} + +export default ExperimentInstanceComponent; diff --git a/react-ui/src/pages/Experiment/components/TensorBoardStatus/index.tsx b/react-ui/src/pages/Experiment/components/TensorBoardStatus/index.tsx index 8a6f5b7c..13168ef1 100644 --- a/react-ui/src/pages/Experiment/components/TensorBoardStatus/index.tsx +++ b/react-ui/src/pages/Experiment/components/TensorBoardStatus/index.tsx @@ -42,7 +42,7 @@ const statusConfig: Record = { }; type TensorBoardStatusProps = { - status: TensorBoardStatus; + status?: TensorBoardStatus; onClick: () => void; }; diff --git a/react-ui/src/pages/Experiment/index.jsx b/react-ui/src/pages/Experiment/index.jsx index 3bbecbb8..e1ab45a5 100644 --- a/react-ui/src/pages/Experiment/index.jsx +++ b/react-ui/src/pages/Experiment/index.jsx @@ -1,31 +1,28 @@ import CommonTableCell from '@/components/CommonTableCell'; import KFIcon from '@/components/KFIcon'; -import { TensorBoardStatus } from '@/enums'; +import { ExperimentStatus, TensorBoardStatus } from '@/enums'; import { deleteExperimentById, - deleteQueryByExperimentInsId, getExperiment, getExperimentById, getQueryByExperimentId, getTensorBoardStatusReq, postExperiment, putExperiment, - putQueryByExperimentInsId, runExperiments, runTensorBoardReq, } from '@/services/experiment/index.js'; import { getWorkflow } from '@/services/pipeline/index.js'; import themes from '@/styles/theme.less'; -import { elapsedTime, formatDate } from '@/utils/date'; import { to } from '@/utils/promise'; import { modalConfirm } from '@/utils/ui'; -import { App, Button, ConfigProvider, Dropdown, Space, Table, Tooltip } from 'antd'; +import { App, Button, ConfigProvider, Dropdown, Space, Table } from 'antd'; import classNames from 'classnames'; import { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { ComparisonType } from './Comparison/config'; import AddExperimentModal from './components/AddExperimentModal'; -import TensorBoardStatusCell from './components/TensorBoardStatus'; +import ExperimentInstance from './components/ExperimentInstance'; import Styles from './index.less'; import { experimentStatusInfo } from './status'; @@ -49,7 +46,18 @@ function Experiment() { const [isAdd, setIsAdd] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); const [addFormData, setAddFormData] = useState({}); + const [experimentInsTotal, setExperimentInsTotal] = useState(0); const { message } = App.useApp(); + const pageOption = useRef({ page: 1, size: 10 }); + const paginationProps = { + showSizeChanger: true, + showQuickJumper: true, + showTotal: () => `共${total}条`, + total: total, + page: pageOption.current.page, + size: pageOption.current.size, + onChange: (current, size) => paginationChange(current, size), + }; useEffect(() => { getList(); @@ -83,38 +91,42 @@ function Experiment() { setWorkflowList(res.data.content); } }; - // 获取实验实例 - const getQueryByExperiment = (val) => { - getQueryByExperimentId(val).then((ret) => { - setExpandedRowKeys(val); - if (ret && ret.data && ret.data.length > 0) { - try { - const list = ret.data.map((v) => { - const nodes_result = v.nodes_result ? JSON.parse(v.nodes_result) : {}; - return { - ...v, - nodes_result, - }; - }); + // 获取实验实例列表 + const getQueryByExperiment = async (experimentId, page) => { + const params = { + experimentId: experimentId, + page: page, + size: 5, + }; + const [res, error] = await to(getQueryByExperimentId(params)); + if (res && res.data) { + const { content = [], totalElements = 0 } = res.data; + setExpandedRowKeys(experimentId); + try { + const list = content.map((v) => { + const nodes_result = v.nodes_result ? JSON.parse(v.nodes_result) : {}; + return { + ...v, + nodes_result, + }; + }); + if (page === 0) { setExperimentInList(list); - // 获取 TensorBoard 状态 - list.forEach((item) => { - if (item.nodes_result?.tensorboard_log) { - const timerId = setTimeout(() => { - getTensorBoardStatus(item); - }, 0); - timerIds.set(item.id, timerId); - } - }); - } catch (error) { - setExperimentInList([]); + clearExperimentInTimers(); + } else { + setExperimentInList((prev) => [...prev, ...list]); } - getList(); - } else { - setExperimentInList([]); - getList(); + setExperimentInsTotal(totalElements); + // 获取 TensorBoard 状态 + list.forEach((item) => { + if (item.nodes_result?.tensorboard_log) { + getTensorBoardStatus(item); + } + }); + } catch (error) { + console.log(error); } - }); + } }; // 运行 TensorBoard const runTensorBoard = async (experimentIn) => { @@ -155,18 +167,26 @@ function Experiment() { return item; }); }); - const timerId = setTimeout(() => { + + let timerId = timerIds.get(experimentIn.id); + if (timerId) { + clearTimeout(timerId); + timerIds.delete(experimentIn.id); + } + timerId = setTimeout(() => { getTensorBoardStatus(experimentIn); - }, 10000); + }, 1000 * 1000); timerIds.set(experimentIn.id, timerId); } }; + // 展开实例 const expandChange = (e, record) => { clearExperimentInTimers(); + setExperimentInList([]); if (record.id === expandedRowKeys) { setExpandedRowKeys(null); } else { - getQueryByExperiment(record.id); + getQueryByExperiment(record.id, 0); } }; // 终止实验实例获取 TensorBoard 状态的定时器 @@ -198,6 +218,7 @@ function Experiment() { const handleCancel = () => { setIsModalOpen(false); }; + // 跳转到流水线 const routeToEdit = (e, record) => { e.stopPropagation(); navgite({ pathname: `/pipeline/template/${record.workflow_id}` }); @@ -226,16 +247,7 @@ function Experiment() { } } }; - const pageOption = useRef({ page: 1, size: 10 }); - const paginationProps = { - showSizeChanger: true, - showQuickJumper: true, - showTotal: () => `共${total}条`, - total: total, - page: pageOption.current.page, - size: pageOption.current.size, - onChange: (current, size) => paginationChange(current, size), - }; + // 当前页面切换 const paginationChange = async (current, size) => { pageOption.current = { @@ -244,21 +256,22 @@ function Experiment() { }; getList(); }; - const runExperiment = (id) => { - runExperiments(id).then((ret) => { - if (ret.code === 200) { - message.success('运行成功'); - getQueryByExperiment(id); - } else { - message.error('运行失败'); - } - }); + // 运行实验 + const runExperiment = async (id) => { + const [res] = await to(runExperiments(id)); + if (res) { + message.success('运行成功'); + refreshExperimentIns(id); + } else { + message.error('运行失败'); + } }; - const routerToText = (e, item, record) => { - e.stopPropagation(); + // 跳转到实验实例详情 + const gotoInstanceInfo = (item, record) => { navgite({ pathname: `/pipeline/experiment/${record.workflow_id}/${item.id}` }); }; + // 处理 TensorBoard 操作 const handleTensorboard = async (experimentIn) => { if ( experimentIn.tensorBoardStatus === TensorBoardStatus.Terminated || @@ -273,6 +286,21 @@ function Experiment() { } }; + // 实验实例终止 + const handleInstanceTerminate = async (experimentIn) => { + setExperimentInList((prevList) => { + return prevList.map((item) => { + if (item.id === experimentIn.id) { + return { + ...item, + status: ExperimentStatus.Terminated, + }; + } + return item; + }); + }); + }; + // 实验对比菜单 const getComparisonMenu = (experimentId) => { return { @@ -292,6 +320,17 @@ function Experiment() { }; }; + // 刷新实验实例列表 + const refreshExperimentIns = (experimentId) => { + getQueryByExperiment(experimentId, 0); + }; + + // 加载更多实验实例 + const loadMoreExperimentIns = () => { + const page = Math.round(experimentInList.length / 5); + getQueryByExperiment(expandedRowKeys, page); + }; + const columns = [ { title: '实验名称', @@ -413,7 +452,7 @@ function Experiment() { ]; return (
-
+
@@ -427,130 +466,16 @@ function Experiment() { scroll={{ y: 'calc(100% - 55px)' }} expandable={{ expandedRowRender: (record) => ( -
- {experimentInList && experimentInList.length > 0 ? ( -
-
序号
-
可视化
-
-
运行时长
-
开始时间
-
-
状态
-
操作
-
- ) : ( - '' - )} - - {experimentInList && experimentInList.length > 0 - ? experimentInList.map((item, index) => ( -
- routerToText(e, item, record)} - > - {index + 1} - -
- {item.nodes_result?.tensorboard_log ? ( - handleTensorboard(item)} - > - ) : ( - '--' - )} -
-
-
- {elapsedTime(item.create_time, item.finish_time)} -
-
- - {formatDate(item.create_time)} - -
-
-
- - - {experimentStatusInfo[item.status]?.label} - -
-
- - - - -
-
- )) - : ''} -
+ gotoInstanceInfo(item, record)} + onClickTensorBoard={handleTensorboard} + onRemove={() => refreshExperimentIns(record.id)} + onTerminate={handleInstanceTerminate} + onLoadMore={() => loadMoreExperimentIns()} + > ), - onExpand: (e, a) => { expandChange(e, a); }, diff --git a/react-ui/src/pages/Experiment/index.less b/react-ui/src/pages/Experiment/index.less index 3d991c4e..526ff59b 100644 --- a/react-ui/src/pages/Experiment/index.less +++ b/react-ui/src/pages/Experiment/index.less @@ -1,92 +1,18 @@ -.experimentTopBox { - display: flex; - align-items: center; - justify-content: flex-end; - width: 100%; - height: 49px; - padding-right: 30px; - background-image: url(/assets/images/pipeline-back.png); - background-repeat: no-repeat; - background-position: top center; - background-size: 100% 100%; -} -.pipelineTopBox { - display: flex; - align-items: center; - justify-content: flex-end; - width: 100%; - height: 49px; - margin-bottom: 10px; - padding-right: 30px; - background-image: url(/assets/images/pipeline-back.png); - background-repeat: no-repeat; - background-position: top center; - background-size: 100% 100%; -} -.tableExpandBox { - display: flex; - align-items: center; - width: 100%; - padding: 0 0 0 33px; - color: @text-color; - font-size: 15px; - - & > div { - padding: 0 16px; - } - - .index { - width: calc((100% + 32px + 33px) / 6.25); - } - - .tensorBoard { - width: calc((100% + 32px + 33px) / 6.25); - } - - .description { +.experimentBox { + height: 100%; + .experimentTopBox { display: flex; - flex: 1; align-items: center; - - .startTime { - .singleLine(); - } - } - - .status { - width: 200px; + justify-content: flex-end; + width: 100%; + height: 49px; + margin-bottom: 10px; + padding-right: 30px; + background-image: url(/assets/images/pipeline-back.png); + background-repeat: no-repeat; + background-position: top center; + background-size: 100% 100%; } - - .operation { - width: 334px; - } -} -.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; -} - -.experimentBox { - height: 100%; .experimentTable { height: calc(100% - 60px); :global { diff --git a/react-ui/src/pages/Mirror/Info/index.tsx b/react-ui/src/pages/Mirror/Info/index.tsx index 60e19ab0..99448356 100644 --- a/react-ui/src/pages/Mirror/Info/index.tsx +++ b/react-ui/src/pages/Mirror/Info/index.tsx @@ -288,7 +288,13 @@ function MirrorInfo() { dataSource={tableData} columns={columns} scroll={{ y: 'calc(100% - 55px)' }} - pagination={{ ...pagination, total, showSizeChanger: true, showQuickJumper: true }} + pagination={{ + ...pagination, + total, + showSizeChanger: true, + showQuickJumper: true, + showTotal: () => `共${total}条`, + }} onChange={handleTableChange} rowKey="id" /> diff --git a/react-ui/src/pages/Mirror/List/index.tsx b/react-ui/src/pages/Mirror/List/index.tsx index f0b2bf77..50e2730e 100644 --- a/react-ui/src/pages/Mirror/List/index.tsx +++ b/react-ui/src/pages/Mirror/List/index.tsx @@ -241,7 +241,7 @@ function MirrorList() {
setInputText(e.target.value)} @@ -277,6 +277,7 @@ function MirrorList() { total: total, showSizeChanger: true, showQuickJumper: true, + showTotal: () => `共${total}条`, }} onChange={handleTableChange} rowKey="id" diff --git a/react-ui/src/pages/ModelDeployment/List/index.tsx b/react-ui/src/pages/ModelDeployment/List/index.tsx index af8fba44..556f3f40 100644 --- a/react-ui/src/pages/ModelDeployment/List/index.tsx +++ b/react-ui/src/pages/ModelDeployment/List/index.tsx @@ -336,6 +336,7 @@ function ModelDeployment() { total: total, showSizeChanger: true, showQuickJumper: true, + showTotal: () => `共${total}条`, }} onChange={handleTableChange} rowKey="service_id" diff --git a/react-ui/src/pages/Pipeline/editPipeline/index.jsx b/react-ui/src/pages/Pipeline/editPipeline/index.jsx index c67c40ed..7d0f4f8e 100644 --- a/react-ui/src/pages/Pipeline/editPipeline/index.jsx +++ b/react-ui/src/pages/Pipeline/editPipeline/index.jsx @@ -86,14 +86,21 @@ const EditPipeline = () => { // 保存 const savePipeline = async (val) => { - const [res, error] = await to(paramsDrawerRef.current.validateFields()); - if (error) { + const [globalParamRes, globalParamError] = await to(paramsDrawerRef.current.validateFields()); + if (globalParamError) { message.error('全局参数配置有误'); openParamsDrawer(); return; } + closeParamsDrawer(); + + const [propsRes, propsError] = await to(propsRef.current.validateFields()); + if (propsError) { + message.error('节点必填项必须配置'); + return; + } + propsRef.current.close(); - propsRef.current.propClose(); setTimeout(() => { const data = graph.save(); console.log(data); @@ -102,16 +109,19 @@ const EditPipeline = () => { }); if (errorNode) { message.error(`【${errorNode.label}】节点必填项必须配置`); + const graphNode = graph.findById(errorNode.id); + if (graphNode) { + openNodeDrawer(graphNode, true); + } return; } const params = { ...locationParams, dag: JSON.stringify(data), - global_param: JSON.stringify(res.global_param), + global_param: JSON.stringify(globalParamRes.global_param), }; saveWorkflow(params).then((ret) => { message.success('保存成功'); - closeParamsDrawer(); setTimeout(() => { if (val) { navgite({ pathname: `/pipeline/template` }); @@ -281,6 +291,17 @@ const EditPipeline = () => { } }; + // 打开节点抽屉 + const openNodeDrawer = (node, validate = false) => { + // 获取所有的上游节点 + const parentNodes = findAllParentNodes(graph, node); + // 如果没有打开过全局参数抽屉,获取不到全局参数 + const globalParams = + paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current; + // 打开节点编辑抽屉 + propsRef.current.showDrawer(node.getModel(), globalParams, parentNodes, validate); + }; + // 初始化图 const initGraph = () => { const contextMenu = initMenu(); @@ -531,13 +552,7 @@ const EditPipeline = () => { const bindEvents = () => { graph.on('node:click', (e) => { if (e.target.get('name') !== 'anchor-point' && e.item) { - // 获取所有的上游节点 - const parentNodes = findAllParentNodes(graph, e.item); - // 如果没有打开过全局参数抽屉,获取不到全局参数 - const globalParams = - paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current; - // 打开节点编辑抽屉 - propsRef.current.showDrawer(e, globalParams, parentNodes); + openNodeDrawer(e.item); } }); graph.on('aftercreateedge', (e) => { diff --git a/react-ui/src/pages/Pipeline/editPipeline/props.tsx b/react-ui/src/pages/Pipeline/editPipeline/props.tsx index f2ff4b5d..af3c1f58 100644 --- a/react-ui/src/pages/Pipeline/editPipeline/props.tsx +++ b/react-ui/src/pages/Pipeline/editPipeline/props.tsx @@ -6,6 +6,7 @@ import { CommonTabKeys } from '@/enums'; import { useComputingResource } from '@/hooks/resource'; import { PipelineGlobalParam, + PipelineNodeModel, PipelineNodeModelParameter, PipelineNodeModelSerialize, } from '@/types'; @@ -57,7 +58,6 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete }; console.log('res', res); - onFormChange(res); } }; @@ -66,36 +66,53 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete }; useImperativeHandle(ref, () => ({ - showDrawer(e: any, params: PipelineGlobalParam[], parentNodes: INode[]) { - if (e.item && e.item.getModel()) { + showDrawer( + model: PipelineNodeModel, + params: PipelineGlobalParam[], + parentNodes: INode[], + validate: boolean = false, + ) { + try { + const nodeData: PipelineNodeModelSerialize = { + ...model, + in_parameters: JSON.parse(model.in_parameters), + out_parameters: JSON.parse(model.out_parameters), + control_strategy: JSON.parse(model.control_strategy), + }; + console.log('model', nodeData); + setStagingItem({ + ...nodeData, + }); form.resetFields(); - const model = e.item.getModel(); - try { - const nodeData = { - ...model, - in_parameters: JSON.parse(model.in_parameters), - out_parameters: JSON.parse(model.out_parameters), - control_strategy: JSON.parse(model.control_strategy), - }; - console.log('model', nodeData); - setStagingItem({ - ...nodeData, - }); - form.setFieldsValue({ - ...nodeData, - }); - } catch (error) { - console.log(error); + form.setFieldsValue({ + ...nodeData, + }); + if (validate) { + form.validateFields(); } - setOpen(true); - - // 参数下拉菜单 - setMenuItems(createMenuItems(params, parentNodes)); + } catch (error) { + console.log(error); } + setOpen(true); + + // 参数下拉菜单 + setMenuItems(createMenuItems(params, parentNodes)); }, - propClose: () => { + close: () => { onClose(); }, + validateFields: async () => { + if (!open) { + return; + } + const [values, error] = await to(form.validateFields()); + if (!error && values) { + return values; + } else { + form.scrollToField((error as any)?.errorFields?.[0]?.name, { block: 'center' }); + return Promise.reject(error); + } + }, })); // 选择数据集、模型、镜像 @@ -279,6 +296,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete maxWidth: 600, }} autoComplete="off" + scrollToFirstError >
diff --git a/react-ui/src/services/experiment/index.js b/react-ui/src/services/experiment/index.js index 66d27eb6..0789dad3 100644 --- a/react-ui/src/services/experiment/index.js +++ b/react-ui/src/services/experiment/index.js @@ -28,9 +28,10 @@ export function deleteExperimentById(id) { }); } // 根据id查询实验实例 -export function getQueryByExperimentId(id) { - return request(`/api/mmp/experimentIns/queryByExperimentId/${id}`, { +export function getQueryByExperimentId(params) { + return request(`/api/mmp/experimentIns`, { method: 'GET', + params, }); } // 根据id删除实验实例 diff --git a/react-ui/src/types.ts b/react-ui/src/types.ts index aa9b0e5e..f443748f 100644 --- a/react-ui/src/types.ts +++ b/react-ui/src/types.ts @@ -4,7 +4,7 @@ * @Description: 定义全局类型,比如无关联的页面都需要要的类型 */ -import { ExperimentStatus } from '@/enums'; +import { ExperimentStatus, TensorBoardStatus } from '@/enums'; // 流水线全局参数 export type PipelineGlobalParam = { @@ -26,9 +26,13 @@ export type ExperimentInstance = { status: string; argo_ins_name: string; argo_ins_ns: string; - nodes_result: string; + nodes_result: { + [key: string]: any; + }; nodes_status: string; global_param: PipelineGlobalParam[]; + tensorBoardStatus?: TensorBoardStatus; + tensorboardUrl?: string; }; // 流水线节点 diff --git a/react-ui/src/utils/promise.ts b/react-ui/src/utils/promise.ts index eab6d2d0..661275ff 100644 --- a/react-ui/src/utils/promise.ts +++ b/react-ui/src/utils/promise.ts @@ -2,12 +2,12 @@ * @param { Promise } promise * @return { Promise } */ -export async function to(promise: Promise): Promise<[T, null] | [null, Error]> { +export async function to(promise: Promise): Promise<[T, null] | [null, U]> { try { const data = await promise; return [data, null]; } catch (error) { - return [null, error as Error]; + return [null, error as U]; } }