From 44673f39edf331e4da31c6f6a82cfa5b5f274427 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Wed, 21 May 2025 15:58:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=9E=E9=AA=8C=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E4=B8=8D=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/components/RunDuration/index.tsx | 11 +- react-ui/src/hooks/useSSE.ts | 32 +- .../src/pages/ActiveLearn/Instance/index.tsx | 11 +- .../components/ActiveLearnBasic/index.tsx | 8 +- react-ui/src/pages/AutoML/Instance/index.tsx | 8 +- .../AutoML/components/AutoMLBasic/index.tsx | 8 +- .../ExperimentInstanceList/index.less | 4 - .../ExperimentInstanceList/index.tsx | 5 +- .../ExperimentInstanceList/instance.tsx | 45 +-- .../components/ExperimentList/config.ts | 13 +- .../components/ExperimentList/index.tsx | 151 +++++++--- .../components/ExperimentRunBasic/index.tsx | 33 ++- react-ui/src/pages/Experiment/Info/index.jsx | 51 ++-- .../ExperimentInstanceList/index.less | 4 - .../ExperimentInstanceList/instance.tsx | 39 ++- react-ui/src/pages/Experiment/index.jsx | 276 ++++++++++-------- .../pages/HyperParameter/Instance/index.tsx | 9 +- .../components/HyperParameterBasic/index.tsx | 8 +- .../components/AssetsManagement/index.tsx | 10 +- react-ui/src/services/activeLearn/index.js | 12 +- react-ui/src/services/autoML/index.js | 14 +- react-ui/src/services/experiment/index.js | 13 +- react-ui/src/services/hyperParameter/index.js | 13 +- react-ui/src/utils/date.ts | 7 +- react-ui/src/utils/experiment.ts | 60 ++++ react-ui/src/utils/index.ts | 24 -- 26 files changed, 551 insertions(+), 318 deletions(-) create mode 100644 react-ui/src/utils/experiment.ts diff --git a/react-ui/src/components/RunDuration/index.tsx b/react-ui/src/components/RunDuration/index.tsx index f430d058..4a9a26cc 100644 --- a/react-ui/src/components/RunDuration/index.tsx +++ b/react-ui/src/components/RunDuration/index.tsx @@ -10,13 +10,21 @@ type RunDurationProps = { }; function RunDuration({ createTime, finishTime, className, style }: RunDurationProps) { const [now] = useServerTime(); - const [currentTime, setCurrentTime] = useState(now()); + const [currentTime, setCurrentTime] = useState(finishTime ? new Date(finishTime) : now()); + + // console.log( + // 'currentTime', + // new Date(createTime ?? 0), + // currentTime, + // (currentTime.getTime() - new Date(createTime ?? 0).getTime()) / 1000, + // ); // 定时刷新耗时 useEffect(() => { if (finishTime) { setCurrentTime(new Date(finishTime)); } else { + setCurrentTime(now()); const timer = setInterval(() => { setCurrentTime(now()); }, 1000); @@ -25,6 +33,7 @@ function RunDuration({ createTime, finishTime, className, style }: RunDurationPr }; } }, [finishTime, now]); + return ( {elapsedTime(createTime, currentTime)} diff --git a/react-ui/src/hooks/useSSE.ts b/react-ui/src/hooks/useSSE.ts index 4ed9d7f2..1d31fdd2 100644 --- a/react-ui/src/hooks/useSSE.ts +++ b/react-ui/src/hooks/useSSE.ts @@ -1,11 +1,24 @@ -import { parseJsonText } from '@/utils'; -import { useEffect } from 'react'; import { ExperimentStatus } from '@/enums'; import { NodeStatus } from '@/types'; +import { parseJsonText } from '@/utils'; +import { useEffect } from 'react'; -export type MessageHandler = (experimentInsId: number, status: string, finishedAt: string, nodes: Record) => void -export const useSSE = (experimentInsId: number, status: ExperimentStatus, name: string, namespace: string, onMessage: MessageHandler) => { - const isRunning = status === ExperimentStatus.Pending || status === ExperimentStatus.Running +export type MessageHandler = ( + experimentId: number, + experimentInsId: number, + status: string, + finishTime: string, + nodes: Record, +) => void; +export const useSSE = ( + experimentId: number, + experimentInsId: number, + status: ExperimentStatus, + name: string, + namespace: string, + onMessage: MessageHandler, +) => { + const isRunning = status === ExperimentStatus.Pending || status === ExperimentStatus.Running; useEffect(() => { if (isRunning) { const { origin } = location; @@ -22,8 +35,8 @@ export const useSSE = (experimentInsId: number, status: ExperimentStatus, name: const dataJson = parseJsonText(data); const statusData = dataJson?.result?.object?.status; if (statusData) { - const { finishedAt, phase, nodes } = statusData; - onMessage(experimentInsId, phase, finishedAt, nodes); + const { finishedAt, phase, nodes } = statusData; + onMessage(experimentId, experimentInsId, phase, finishedAt, nodes); } }; @@ -33,8 +46,7 @@ export const useSSE = (experimentInsId: number, status: ExperimentStatus, name: return () => { evtSource.close(); - } + }; } - - }, [experimentInsId, isRunning, name, namespace, onMessage]); + }, [experimentId, experimentInsId, isRunning, name, namespace, onMessage]); }; diff --git a/react-ui/src/pages/ActiveLearn/Instance/index.tsx b/react-ui/src/pages/ActiveLearn/Instance/index.tsx index f0fa9e21..d8263d78 100644 --- a/react-ui/src/pages/ActiveLearn/Instance/index.tsx +++ b/react-ui/src/pages/ActiveLearn/Instance/index.tsx @@ -68,7 +68,7 @@ function ActiveLearnInstance() { return; } - // 设置节点状态 + // 设置总 workflow 状态 const nodeStatusJson = parseJsonText(node_status); if (nodeStatusJson) { setNodes(nodeStatusJson); @@ -105,18 +105,17 @@ function ActiveLearnInstance() { if (dataJson) { const nodes = dataJson?.result?.object?.status?.nodes; if (nodes) { - // 节点 + // 设置节点 setNodes(nodes); + // 设置总 workflow 状态 const workflowStatus = Object.values(nodes).find((node: any) => node.displayName.startsWith(NodePrefix), ) as NodeStatus; - - // 设置工作流状态 if (workflowStatus) { setWorkflowStatus(workflowStatus); - // 实验结束,关闭 SSE + // 实验结束,关闭 SSE,获取实验实例结果 if ( workflowStatus.phase !== ExperimentStatus.Pending && workflowStatus.phase !== ExperimentStatus.Running @@ -151,7 +150,7 @@ function ActiveLearnInstance() { diff --git a/react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.tsx b/react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.tsx index de900161..e0decf8b 100644 --- a/react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.tsx +++ b/react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.tsx @@ -28,14 +28,14 @@ type BasicInfoProps = { info?: ActiveLearnData; className?: string; isInstance?: boolean; - runStatus?: NodeStatus; + workflowStatus?: NodeStatus; instanceStatus?: ExperimentStatus; }; function BasicInfo({ info, className, - runStatus, + workflowStatus, instanceStatus, isInstance = false, }: BasicInfoProps) { @@ -212,8 +212,8 @@ function BasicInfo({ return (
- {isInstance && runStatus && ( - + {isInstance && workflowStatus && ( + )} {!isInstance && ( node.displayName.startsWith(NodePrefix), ) as NodeStatus; - if (workflowStatus) { setWorkflowStatus(workflowStatus); - // 实验结束,关闭 SSE + // 实验结束,关闭 SSE,获取实验实例结果 if ( workflowStatus.phase !== ExperimentStatus.Pending && workflowStatus.phase !== ExperimentStatus.Running @@ -157,7 +157,7 @@ function AutoMLInstance() { diff --git a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx index be8884fc..56525f00 100644 --- a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx +++ b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx @@ -38,7 +38,7 @@ type AutoMLBasicProps = { info?: AutoMLData; className?: string; isInstance?: boolean; - runStatus?: NodeStatus; + workflowStatus?: NodeStatus; instanceStatus?: ExperimentStatus; instanceCreateTime?: string; }; @@ -46,7 +46,7 @@ type AutoMLBasicProps = { function AutoMLBasic({ info, className, - runStatus, + workflowStatus, instanceStatus, isInstance = false, }: AutoMLBasicProps) { @@ -293,8 +293,8 @@ function AutoMLBasic({ return (
- {isInstance && runStatus && ( - + {isInstance && workflowStatus && ( + )} {!isInstance && ( {index + 1} - +
- - - {experimentStatusInfo[status]?.label} - + {statusInfo ? ( + <> + + {statusInfo.label} + + ) : ( + '--' + )}
); diff --git a/react-ui/src/pages/AutoML/components/ExperimentList/config.ts b/react-ui/src/pages/AutoML/components/ExperimentList/config.ts index cc61a7e8..f1b1bf66 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentList/config.ts +++ b/react-ui/src/pages/AutoML/components/ExperimentList/config.ts @@ -8,6 +8,7 @@ import { batchDeleteActiveLearnInsReq, deleteActiveLearnInsReq, deleteActiveLearnReq, + editActiveLearnInsReq, getActiveLearnInsListReq, getActiveLearnListReq, runActiveLearnReq, @@ -17,6 +18,7 @@ import { batchDeleteExperimentInsReq, deleteAutoMLReq, deleteExperimentInsReq, + editExperimentInsReq, getAutoMLListReq, getExperimentInsListReq, runAutoMLReq, @@ -26,6 +28,7 @@ import { batchDeleteRayInsReq, deleteRayInsReq, deleteRayReq, + editRayInsReq, getRayInsListReq, getRayListReq, runRayReq, @@ -40,17 +43,19 @@ export enum ExperimentListType { type ExperimentListInfo = { getListReq: (params: any, skipLoading?: boolean) => Promise; // 获取列表 - getInsListReq: (params: any) => Promise; // 获取实例列表 + getInsListReq: (params: any, skipLoading?: boolean) => Promise; // 获取实例列表 deleteRecordReq: (params: any) => Promise; // 删除 runRecordReq: (params: any) => Promise; // 运行 deleteInsReq: (params: any) => Promise; // 删除实例 batchDeleteInsReq: (params: any) => Promise; // 批量删除实例 stopInsReq: (params: any) => Promise; // 终止实例 + editInsReq: (params: any) => Promise; // 编辑实例 title: string; // 标题 pathPrefix: string; // 路由路径前缀 idProperty: string; // ID属性 nameProperty: string; // 名称属性 descProperty: string; // 描述属性 + idInsProperty: string; // 实例返回的ID属性 }; export const experimentListConfig: Record = { @@ -62,11 +67,13 @@ export const experimentListConfig: Record([]); const [expandedRowKeys, setExpandedRowKeys] = useState([]); const [experimentInsTotal, setExperimentInsTotal] = useState(0); - const [now] = useServerTime(); const [pagination, setPagination] = useState( cacheState?.pagination ?? { current: 1, @@ -60,10 +59,10 @@ function ExperimentList({ type }: ExperimentListProps) { }, ); const config = experimentListConfig[type]; - const timerRef = useRef | undefined>(); + const [now] = useServerTime(); - // 获取自主机器学习或超参数自动优化列表 - const getAutoMLList = useCallback( + // 获取实验列表 + const getExperimentList = useCallback( async (skipLoading: boolean = false) => { const params: Record = { page: pagination.current! - 1, @@ -81,20 +80,16 @@ function ExperimentList({ type }: ExperimentListProps) { [pagination, searchText, config], ); - useEffect(() => { - getAutoMLList(); - }, [getAutoMLList]); - // 获取实验实例列表 const getExperimentInsList = useCallback( - async (recordId: number, page: number, size: number) => { + async (recordId: number, page: number, size: number, skipLoading: boolean = false) => { const params = { [config.idProperty]: recordId, page: page, size: size, }; const request = config.getInsListReq; - const [res] = await to(request(params)); + const [res] = await to(request(params, skipLoading)); if (res && res.data) { const { content = [], totalElements = 0 } = res.data; try { @@ -116,60 +111,113 @@ function ExperimentList({ type }: ExperimentListProps) { // TODO: 目前是直接刷新实验列表,后续需要优化,只刷新状态 const refreshExperimentList = useCallback( (skipLoading: boolean = false) => { - getAutoMLList(skipLoading); + getExperimentList(skipLoading); }, - [getAutoMLList], + [getExperimentList], ); // 刷新实验实例列表 const refreshExperimentIns = useCallback( - (experimentId: number) => { + (experimentId: number, skipLoading: boolean = false) => { const length = experimentInsList.length; - getExperimentInsList(experimentId, 0, length); + getExperimentInsList(experimentId, 0, length, skipLoading); }, [getExperimentInsList, experimentInsList], ); - // 新增,删除版本时,重置分页,然后刷新版本列表 + // 更新实验实例状态 + const editExperimentIns = useCallback( + async ( + experimentId: number, + experimentInsId: number, + status: ExperimentStatus, + argo_ins_name: string, + argo_ins_ns: string, + ) => { + const params = { + [config.idInsProperty]: experimentId, + id: experimentInsId, + status: status, + argo_ins_name, + argo_ins_ns, + }; + const request = config.editInsReq; + const [res] = await to(request(params)); + if (res && res.data) { + refreshExperimentIns(experimentId, true); + refreshExperimentList(true); + } + }, + [config, refreshExperimentIns, refreshExperimentList], + ); + + // 获取实验列表 + useEffect(() => { + getExperimentList(); + }, [getExperimentList]); + + // expandedRowKeys 变化 + useEffect(() => { + if (expandedRowKeys.length > 0) { + getExperimentInsList(expandedRowKeys[0], 0, 5); + refreshExperimentList(); + } + }, [expandedRowKeys, getExperimentInsList, refreshExperimentList]); + + // 实验实例状态变化 useEffect(() => { const handleMessage = (e: MessageEvent) => { const { type, payload } = e.data; if (type === ExperimentCompleted) { - const { id, status, finish_time } = payload; - - // 修改实例的状态和结束时间 - setExperimentInsList((prev) => - prev.map((v) => - v.id === id - ? { - ...v, - status: status, - finish_time: finish_time, - } - : v, - ), + const { experimentId, experimentInsId, status, finishTime } = payload; + const currentIns = experimentInsList.find((v) => v.id === experimentInsId); + console.log( + '实验实例状态变化', + currentIns?.status, + status, + experimentId, + experimentInsId, + finishTime, ); - if (timerRef.current) { - clearTimeout(timerRef.current); - timerRef.current = undefined; + if ( + !currentIns || + currentIns.status === ExperimentStatus.Terminated || + currentIns.status === status + ) { + return; } - timerRef.current = setTimeout(() => { - refreshExperimentList(true); - }, 10000); + // refreshExperimentList(true); + // refreshExperimentIns(experimentId); + editExperimentIns( + experimentId, + experimentInsId, + status, + currentIns.argo_ins_name, + currentIns.argo_ins_ns, + ); + + // 修改实例的状态和结束时间 + // setExperimentInsList((prev) => + // prev.map((v) => + // v.id === experimentInsId + // ? { + // ...v, + // status: status, + // finish_time: finishTime, + // } + // : v, + // ), + // ); } }; window.addEventListener('message', handleMessage); return () => { window.removeEventListener('message', handleMessage); - if (timerRef.current) { - clearTimeout(timerRef.current); - timerRef.current = undefined; - } }; - }, [refreshExperimentList]); + }, [experimentInsList, editExperimentIns]); // 搜索 const onSearch: SearchProps['onSearch'] = (value) => { @@ -213,6 +261,7 @@ function ExperimentList({ type }: ExperimentListProps) { setCacheState({ pagination, searchText, + expandedRowKeys, }); if (record) { @@ -231,6 +280,7 @@ function ExperimentList({ type }: ExperimentListProps) { setCacheState({ pagination, searchText, + expandedRowKeys, }); navigate(`info/${record.id}`); @@ -243,8 +293,8 @@ function ExperimentList({ type }: ExperimentListProps) { if (res) { message.success('运行成功'); setExpandedRowKeys([record.id]); - refreshExperimentList(); - getExperimentInsList(record.id, 0, 5); + // getExperimentInsList(record.id, 0, 5); + // refreshExperimentList(); } }; @@ -254,8 +304,8 @@ function ExperimentList({ type }: ExperimentListProps) { setExperimentInsList([]); if (expanded) { setExpandedRowKeys([record.id]); - getExperimentInsList(record.id, 0, 5); - refreshExperimentList(); + // getExperimentInsList(record.id, 0, 5); + // refreshExperimentList(); } else { setExpandedRowKeys([]); } @@ -263,6 +313,11 @@ function ExperimentList({ type }: ExperimentListProps) { // 跳转到实验实例详情 const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => { + setCacheState({ + pagination, + searchText, + expandedRowKeys, + }); navigate(`instance/${autoML.id}/${record.id}`); }; @@ -275,8 +330,7 @@ function ExperimentList({ type }: ExperimentListProps) { // 实验实例终止 const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => { - // 刷新实验列表 - refreshExperimentList(); + // 修改实例的状态和结束时间 setExperimentInsList((prevList) => { return prevList.map((item) => { if (item.id === experimentIns.id) { @@ -289,6 +343,11 @@ function ExperimentList({ type }: ExperimentListProps) { return item; }); }); + // 刷新实验列表 + refreshExperimentList(true); + if (expandedRowKeys.length > 0) { + refreshExperimentIns(expandedRowKeys[0]); + } }; // --------------------------- Table --------------------------- // 分页切换 diff --git a/react-ui/src/pages/AutoML/components/ExperimentRunBasic/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentRunBasic/index.tsx index 4b185811..14ec8207 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentRunBasic/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentRunBasic/index.tsx @@ -3,58 +3,61 @@ import RunDuration from '@/components/RunDuration'; import { ExperimentStatus } from '@/enums'; import { experimentStatusInfo } from '@/pages/Experiment/status'; import { type NodeStatus } from '@/types'; +import { getExperimentInstanceStatus } from '@/utils/experiment'; import { formatDate } from '@/utils/format'; import { Flex } from 'antd'; import { useMemo } from 'react'; type ExperimentRunBasicProps = { - runStatus?: NodeStatus; + workflowStatus?: NodeStatus; instanceStatus?: ExperimentStatus; }; -function ExperimentRunBasic({ runStatus, instanceStatus }: ExperimentRunBasicProps) { +function ExperimentRunBasic({ workflowStatus, instanceStatus }: ExperimentRunBasicProps) { const instanceDatas = useMemo(() => { - if (!runStatus) { - return []; - } - - const status = - instanceStatus === ExperimentStatus.Terminated ? instanceStatus : runStatus.phase; + const status = getExperimentInstanceStatus(instanceStatus as ExperimentStatus, workflowStatus); const statusInfo = experimentStatusInfo[status]; return [ { label: '启动时间', - value: formatDate(runStatus.startedAt), + value: formatDate(workflowStatus?.startedAt), }, { label: '执行时长', - value: , + value: ( + + ), }, { label: '状态', - value: ( + value: statusInfo ? (
- {statusInfo?.label} + {statusInfo.label}
+ ) : ( + '--' ), }, ]; - }, [runStatus, instanceStatus]); + }, [workflowStatus, instanceStatus]); return ( { initGraph(); @@ -92,16 +95,17 @@ function ExperimentText() { const workflowData = workflowRef.current; const experimentStatusObjs = parseJsonText(nodes_status); if (experimentStatusObjs) { + // 更新各个节点 workflowData.nodes.forEach((item) => { const experimentNode = experimentStatusObjs?.[item.id]; updateWorkflowNode(item, experimentNode); }); - // 处理workflow状态 + // 设置 workflow 总状态 Object.keys(experimentStatusObjs).some((key) => { if (key.startsWith(NodePrefix)) { - const workflowStatus = experimentStatusObjs[key]; - setWorkflowStatus(workflowStatus); + const tempWorkflowStatus = experimentStatusObjs[key]; + setWorkflowStatus(tempWorkflowStatus); return true; } return false; @@ -154,27 +158,30 @@ function ExperimentText() { if (!statusData) { return; } - const { startedAt, finishedAt, phase, nodes = {} } = statusData; + const { finishedAt, phase, nodes = {} } = statusData; + // 更新实验实例状态和结束时间 setExperimentIns((prev) => ({ ...prev, finish_time: finishedAt, status: phase, })); - const workflowStatus = Object.values(nodes).find((node) => + // 设置总 workflow 状态 + const tempWorkflowStatus = Object.values(nodes).find((node) => node.displayName.startsWith(NodePrefix), ); - - // 设置工作流状态 - if (workflowStatus) { - setWorkflowStatus(workflowStatus); + if (tempWorkflowStatus) { + setWorkflowStatus(tempWorkflowStatus); } + // 更新各个节点 const workflowData = workflowRef.current; workflowData.nodes.forEach((item) => { const experimentNode = Object.values(nodes).find((node) => node.displayName === item.id); updateWorkflowNode(item, experimentNode); }); + + // 绘制图 getGraphData(workflowData, false); // 更新打开的抽屉数据 @@ -200,6 +207,7 @@ function ExperimentText() { evtSourceRef.current = evtSource; }; + // 更新各个节点 function updateWorkflowNode(workflowNode, statusNode) { if (!statusNode) { return; @@ -505,18 +513,19 @@ function ExperimentText() {
状态: -
- - {experimentStatusInfo[workflowStatus?.phase]?.label} - + {statusInfo ? ( + <> + + {statusInfo.label} + + ) : ( + '--' + )}
- - - {experimentStatusInfo[status]?.label} - + {statusInfo ? ( + <> + + {statusInfo.label} + + ) : ( + '--' + )}
); diff --git a/react-ui/src/pages/Experiment/index.jsx b/react-ui/src/pages/Experiment/index.jsx index 58ff4f63..9705ede5 100644 --- a/react-ui/src/pages/Experiment/index.jsx +++ b/react-ui/src/pages/Experiment/index.jsx @@ -5,6 +5,7 @@ import { useCacheState } from '@/hooks/useCacheState'; import { useServerTime } from '@/hooks/useServerTime'; import { deleteExperimentById, + editExperimentInsReq, getExperiment, getExperimentById, getQueryByExperimentId, @@ -40,7 +41,7 @@ function Experiment() { const [workflowList, setWorkflowList] = useState([]); const [experimentId, setExperimentId] = useState(null); const [experimentInsList, setExperimentInsList] = useState([]); - const [expandedRowKeys, setExpandedRowKeys] = useState(null); + const [expandedRowKeys, setExpandedRowKeys] = useState([]); const [total, setTotal] = useState(0); const [isAdd, setIsAdd] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); @@ -61,7 +62,7 @@ function Experiment() { // 获取实验列表 const getExperimentList = useCallback( - async (skipLoading) => { + async (skipLoading = false) => { const params = { page: pagination.current - 1, size: pagination.pageSize, @@ -84,12 +85,114 @@ function Experiment() { // 刷新实验列表状态, // 目前是直接刷新实验列表,后续需要优化,只刷新状态 const refreshExperimentList = useCallback( - (skipLoading) => { + (skipLoading = false) => { getExperimentList(skipLoading); }, [getExperimentList], ); + // 获取 TensorBoard 状态 + const getTensorBoardStatus = useCallback(async (experimentIn) => { + const params = { + namespace: experimentIn.nodes_result.tensorboard_log.namespace, + path: experimentIn.nodes_result.tensorboard_log.path, + pvc_name: experimentIn.nodes_result.tensorboard_log.pvc_name, + }; + const [res] = await to(getTensorBoardStatusReq(params)); + if (res && res.data) { + setExperimentInsList((prevList) => { + return prevList.map((item) => { + if (item.id === experimentIn.id) { + return { + ...item, + tensorBoardStatus: res.data.status, + tensorboardUrl: res.data.url, + }; + } + return item; + }); + }); + + let timerId = timerIds.get(experimentIn.id); + if (timerId) { + clearTimeout(timerId); + timerIds.delete(experimentIn.id); + } + timerId = setTimeout(() => { + getTensorBoardStatus(experimentIn); + }, 10 * 1000); + timerIds.set(experimentIn.id, timerId); + } + }, []); + + // 获取实验实例列表 + const getExperimentInsList = useCallback( + async (experimentId, page, size = 5, skipLoading = false) => { + const params = { + experimentId: experimentId, + page: page, + size: size, + }; + const [res, error] = await to(getQueryByExperimentId(params, skipLoading)); + if (res && res.data) { + const { content = [], totalElements = 0 } = res.data; + try { + const list = content.map((v) => { + const nodes_result = v.nodes_result ? JSON.parse(v.nodes_result) : {}; + return { + ...v, + nodes_result, + }; + }); + if (page === 0) { + setExperimentInsList(list); + clearExperimentInTimers(); + } else { + setExperimentInsList((prev) => [...prev, ...list]); + } + setExperimentInsTotal(totalElements); + // 获取 TensorBoard 状态 + list.forEach((item) => { + if (item.nodes_result?.tensorboard_log) { + getTensorBoardStatus(item); + } + }); + } catch (error) { + console.error('JSON parse error: ', error); + } + } + }, + [getTensorBoardStatus], + ); + + // 刷新实验实例列表 + const refreshExperimentIns = useCallback( + (experimentId, skipLoading = false) => { + const length = experimentInsList.length; + getExperimentInsList(experimentId, 0, length, skipLoading); + }, + [experimentInsList, getExperimentInsList], + ); + + // 更新实验状态 + const editExperimentIns = useCallback( + async (experimentId, experimentInsId, status, argo_ins_name, argo_ins_ns) => { + const params = { + experiment_id: experimentId, + id: experimentInsId, + status: status, + argo_ins_name, + argo_ins_ns, + }; + const [res, error] = await to(editExperimentInsReq(params)); + if (res && res.data) { + refreshExperimentIns(experimentId, true); + refreshExperimentList(true); + } + }, + [refreshExperimentIns, refreshExperimentList], + ); + // 获取流水线列表 useEffect(() => { // 获取流水线列表 @@ -111,52 +214,66 @@ function Experiment() { clearExperimentInTimers(); }; }, []); - // 获取实验列表 useEffect(() => { getExperimentList(); }, [getExperimentList]); - // 新增,删除版本时,重置分页,然后刷新版本列表 + // 更新实验实例状态 useEffect(() => { const handleMessage = (e) => { const { type, payload } = e.data; if (type === ExperimentCompleted) { - const { id, status, finish_time } = payload; - - // 修改实例的状态和结束时间 - setExperimentInsList((prev) => - prev.map((v) => - v.id === id - ? { - ...v, - status: status, - finish_time: finish_time, - } - : v, - ), + const { experimentId, experimentInsId, status, finishTime } = payload; + const currentIns = experimentInsList.find((v) => v.id === experimentInsId); + console.log( + '实验实例状态变化', + currentIns?.status, + status, + experimentId, + experimentInsId, + finishTime, ); - if (timerRef.current) { - clearTimeout(timerRef.current); - timerRef.current = undefined; + if ( + !currentIns || + currentIns.status === ExperimentStatus.Terminated || + currentIns.status === status + ) { + return; } - timerRef.current = setTimeout(() => { - refreshExperimentList(true); - }, 10000); + editExperimentIns( + experimentId, + experimentInsId, + status, + currentIns.argo_ins_name, + currentIns.argo_ins_ns, + ); + + // refreshExperimentList(true); + // refreshExperimentIns(experimentId); + + // 修改实例的状态和结束时间 + // setExperimentInsList((prev) => + // prev.map((v) => + // v.id === experimentInsId + // ? { + // ...v, + // status: status, + // finish_time: finishTime, + // } + // : v, + // ), + // ); } }; window.addEventListener('message', handleMessage); return () => { window.removeEventListener('message', handleMessage); - if (timerRef.current) { - clearTimeout(timerRef.current); - timerRef.current = undefined; - } }; - }, [refreshExperimentList]); + }, [experimentInsList, editExperimentIns]); // 搜索 const onSearch = (value) => { @@ -167,44 +284,6 @@ function Experiment() { })); }; - // 获取实验实例列表 - const getQueryByExperiment = async (experimentId, page, size = 5) => { - const params = { - experimentId: experimentId, - page: page, - size: size, - }; - 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) { - setExperimentInsList(list); - clearExperimentInTimers(); - } else { - setExperimentInsList((prev) => [...prev, ...list]); - } - setExperimentInsTotal(totalElements); - // 获取 TensorBoard 状态 - list.forEach((item) => { - if (item.nodes_result?.tensorboard_log) { - getTensorBoardStatus(item); - } - }); - } catch (error) { - console.error('JSON parse error: ', error); - } - } - }; - // 运行 TensorBoard const runTensorBoard = async (experimentIn) => { const params = { @@ -224,49 +303,16 @@ function Experiment() { } }; - // 获取 TensorBoard 状态 - const getTensorBoardStatus = async (experimentIn) => { - const params = { - namespace: experimentIn.nodes_result.tensorboard_log.namespace, - path: experimentIn.nodes_result.tensorboard_log.path, - pvc_name: experimentIn.nodes_result.tensorboard_log.pvc_name, - }; - const [res] = await to(getTensorBoardStatusReq(params)); - if (res && res.data) { - setExperimentInsList((prevList) => { - return prevList.map((item) => { - if (item.id === experimentIn.id) { - return { - ...item, - tensorBoardStatus: res.data.status, - tensorboardUrl: res.data.url, - }; - } - return item; - }); - }); - - let timerId = timerIds.get(experimentIn.id); - if (timerId) { - clearTimeout(timerId); - timerIds.delete(experimentIn.id); - } - timerId = setTimeout(() => { - getTensorBoardStatus(experimentIn); - }, 10 * 1000); - timerIds.set(experimentIn.id, timerId); - } - }; - // 展开实例 - const expandChange = (e, record) => { + const expandChange = (expanded, record) => { clearExperimentInTimers(); setExperimentInsList([]); - if (record.id === expandedRowKeys) { - setExpandedRowKeys(null); - } else { - getQueryByExperiment(record.id, 0, 5); + if (expanded) { + setExpandedRowKeys([record.id]); + getExperimentInsList(record.id, 0, 5); refreshExperimentList(); + } else { + setExpandedRowKeys([]); } }; @@ -344,8 +390,9 @@ function Experiment() { const [res] = await to(runExperiments(id)); if (res) { message.success('运行成功'); + setExpandedRowKeys([id]); refreshExperimentList(); - getQueryByExperiment(id, 0, 5); + getExperimentInsList(id, 0, 5); } }; @@ -388,8 +435,6 @@ function Experiment() { // 实验实例终止 const handleInstanceTerminate = async (experimentIn) => { - // 刷新实验列表 - refreshExperimentList(); setExperimentInsList((prevList) => { return prevList.map((item) => { if (item.id === experimentIn.id) { @@ -402,6 +447,9 @@ function Experiment() { return item; }); }); + // 刷新实验列表 + refreshExperimentList(true); + refreshExperimentIns(experimentIn.experiment_id); }; // 实验对比菜单 @@ -423,16 +471,10 @@ function Experiment() { }; }; - // 刷新实验实例列表 - const refreshExperimentIns = (experimentId) => { - const length = experimentInsList.length; - getQueryByExperiment(experimentId, 0, length); - }; - // 加载更多实验实例 const loadMoreExperimentIns = () => { const page = Math.round(experimentInsList.length / 5); - getQueryByExperiment(expandedRowKeys, page, 5); + getExperimentInsList(expandedRowKeys, page, 5); }; // 处理删除 @@ -617,7 +659,7 @@ function Experiment() { > ), onExpand: expandChange, - expandedRowKeys: [expandedRowKeys], + expandedRowKeys: expandedRowKeys, }} /> diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx index 19c485a0..3605315f 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.tsx +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -120,18 +120,17 @@ function HyperParameterInstance() { if (dataJson) { const nodes = dataJson?.result?.object?.status?.nodes; if (nodes) { - // 节点 + // 设置节点 setNodes(nodes); + // 设置总 workflow 状态 const workflowStatus = Object.values(nodes).find((node: any) => node.displayName.startsWith(NodePrefix), ) as NodeStatus; - - // 设置工作流状态 if (workflowStatus) { setWorkflowStatus(workflowStatus); - // 实验结束,关闭 SSE + // 实验结束,关闭 SSE,获取实验实例结果 if ( workflowStatus.phase !== ExperimentStatus.Pending && workflowStatus.phase !== ExperimentStatus.Running @@ -166,7 +165,7 @@ function HyperParameterInstance() { diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx index 4c52ea58..282a5667 100644 --- a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx @@ -30,14 +30,14 @@ type HyperParameterBasicProps = { info?: HyperParameterData; className?: string; isInstance?: boolean; - runStatus?: NodeStatus; + workflowStatus?: NodeStatus; instanceStatus?: ExperimentStatus; }; function HyperParameterBasic({ info, className, - runStatus, + workflowStatus, instanceStatus, isInstance = false, }: HyperParameterBasicProps) { @@ -144,8 +144,8 @@ function HyperParameterBasic({ return (
- {isInstance && runStatus && ( - + {isInstance && workflowStatus && ( + )} {!isInstance && ( { + if (!node_status) { + return undefined; + } + + const nodeStatusJson = parseJsonText(node_status); + if (!nodeStatusJson) { + return undefined; + } + + for (const key in nodeStatusJson) { + if (key.startsWith('workflow')) { + return nodeStatusJson[key]; + } + } + return undefined; +}; + +/** + * 获取实例状态 + * 终止或者 workflowStatus 不存在时,取实例状态,否则取流水线状态 + * + * @param instanceStatus - 实例状态 + * @param workflowStatus - 流水线workflow节点 + * @return 实例状态 + */ +export const getExperimentInstanceStatus = ( + instanceStatus: ExperimentStatus, + workflowStatus?: NodeStatus, +): ExperimentStatus => { + return instanceStatus === ExperimentStatus.Terminated || !workflowStatus + ? instanceStatus + : workflowStatus?.phase; +}; + +/** + * 获取实例状态 + * 终止或者 workflowStatus 不存在时,取实例状态,否则取流水线状态 + * + * @param instanceStatus - 实例状态 + * @param workflowStatus - 流水线workflow节点 + * @return 实例状态 + */ +export const getInstanceStatusInList = ( + instanceStatus: ExperimentStatus, + node_status?: string | null, +): ExperimentStatus => { + const workflowStatus = getWorkflowStatus(node_status); + return getExperimentInstanceStatus(instanceStatus, workflowStatus); +}; diff --git a/react-ui/src/utils/index.ts b/react-ui/src/utils/index.ts index 0abf1a45..a62e6e88 100644 --- a/react-ui/src/utils/index.ts +++ b/react-ui/src/utils/index.ts @@ -352,27 +352,3 @@ export const trimCharacter = (str: string, ch: string): string => { export const convertEmptyStringToUndefined = (value?: string): string | undefined => { return value === '' ? undefined : value; }; - -/** - * 获取工作流节点 - * - * @param node_status - the status of the node - * @return the workflow node - */ -export const getWorkflowStatus = (node_status?: string | null) => { - if (!node_status) { - return; - } - - const nodeStatusJson = parseJsonText(node_status); - if (!nodeStatusJson) { - return; - } - - for (const key in nodeStatusJson) { - if (key.startsWith('workflow')) { - return nodeStatusJson[key]; - } - } - return; -};