From 58bed49279d8f1a7478214a647e58e23ee3f9f03 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Mon, 8 Jul 2024 09:39:44 +0800 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/pages/Experiment/Info/index.jsx | 2 ++ react-ui/src/pages/Experiment/Info/props.tsx | 11 ++--------- .../pages/Experiment/components/LogGroup/index.tsx | 2 +- .../src/pages/Experiment/components/LogList/index.tsx | 8 +++++++- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/react-ui/src/pages/Experiment/Info/index.jsx b/react-ui/src/pages/Experiment/Info/index.jsx index 3c1eda2f..1d883daa 100644 --- a/react-ui/src/pages/Experiment/Info/index.jsx +++ b/react-ui/src/pages/Experiment/Info/index.jsx @@ -112,6 +112,8 @@ function ExperimentText() { }, 5 * 1000); } + // 如果状态是 Pending, 打开第一个节点 + // 如果状态是 Running,打开第一个运行中的节点,如果没有运行中的节点,打开第一个节点 if (first && status === ExperimentStatus.Pending) { const node = workflowData.nodes[0]; if (node) { diff --git a/react-ui/src/pages/Experiment/Info/props.tsx b/react-ui/src/pages/Experiment/Info/props.tsx index 5940756a..4f852716 100644 --- a/react-ui/src/pages/Experiment/Info/props.tsx +++ b/react-ui/src/pages/Experiment/Info/props.tsx @@ -10,13 +10,6 @@ import LogList from '../components/LogList'; import { experimentStatusInfo } from '../status'; import styles from './props.less'; -export type ExperimentLog = { - log_type: 'normal' | 'resource'; // 日志类型 - pod_name?: string; // 分布式名称 - log_content?: string; // 日志内容 - start_time?: string; // 日志开始时间 -}; - type ExperimentDrawerProps = { open: boolean; onClose: () => void; @@ -25,8 +18,8 @@ type ExperimentDrawerProps = { instanceNamespace?: string; // 实验实例 namespace instanceNodeData: PipelineNodeModelSerialize; // 节点数据,在定时刷新实验实例状态中不会变化 workflowId?: string; // 实验实例工作流 id - instanceNodeStatus?: ExperimentStatus; // 在定时刷新实验实例状态中,变化一两次 - instanceNodeStartTime?: string; // 在定时刷新实验实例状态中,变化一两次 + instanceNodeStatus?: ExperimentStatus; // 实例节点状态 + instanceNodeStartTime?: string; // 开始时间 instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化 }; diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx index ffb6eb1d..4440c9cb 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx @@ -6,12 +6,12 @@ import { ExperimentStatus } from '@/enums'; import { useStateRef } from '@/hooks'; -import { ExperimentLog } from '@/pages/Experiment/Info/props'; import { getExperimentPodsLog } from '@/services/experiment/index.js'; import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons'; import { Button } from 'antd'; import classNames from 'classnames'; import { useEffect, useRef, useState } from 'react'; +import { ExperimentLog } from '../LogList'; import styles from './index.less'; export type LogGroupProps = ExperimentLog & { diff --git a/react-ui/src/pages/Experiment/components/LogList/index.tsx b/react-ui/src/pages/Experiment/components/LogList/index.tsx index f9fbccea..911bf5d9 100644 --- a/react-ui/src/pages/Experiment/components/LogList/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogList/index.tsx @@ -1,5 +1,4 @@ import { ExperimentStatus } from '@/enums'; -import { ExperimentLog } from '@/pages/Experiment/Info/props'; import { getQueryByExperimentLog } from '@/services/experiment/index.js'; import { to } from '@/utils/promise'; import dayjs from 'dayjs'; @@ -7,6 +6,13 @@ import { useEffect, useState } from 'react'; import LogGroup from '../LogGroup'; import styles from './index.less'; +export type ExperimentLog = { + log_type: 'normal' | 'resource'; // 日志类型 + pod_name?: string; // 分布式名称 + log_content?: string; // 日志内容 + start_time?: string; // 日志开始时间 +}; + type LogListProps = { instanceName?: string; // 实验实例 name instanceNamespace?: string; // 实验实例 namespace From 3a709655b737f5f9f1d8b37b87f5553881f69176 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Tue, 9 Jul 2024 15:01:31 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E5=AE=9E=E9=AA=8C=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=E8=BF=90=E8=A1=8C=E7=8A=B6=E6=80=81=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=20SSE=20=E8=AF=B7=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/package.json | 2 +- react-ui/src/pages/Experiment/Info/index.jsx | 116 +++++++++++++----- .../src/pages/Pipeline/editPipeline/index.jsx | 8 +- 3 files changed, 90 insertions(+), 36 deletions(-) diff --git a/react-ui/package.json b/react-ui/package.json index cc1a3278..cab066ed 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -32,7 +32,7 @@ "record": "cross-env NODE_ENV=development REACT_APP_ENV=test max record --scene=login", "serve": "umi-serve", "start": "cross-env UMI_ENV=dev max dev", - "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev", + "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev UMI_DEV_SERVER_COMPRESS=none max dev", "start:mock": "cross-env REACT_APP_ENV=dev UMI_ENV=dev max dev", "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev", "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev", diff --git a/react-ui/src/pages/Experiment/Info/index.jsx b/react-ui/src/pages/Experiment/Info/index.jsx index 1d883daa..f2a5fd6c 100644 --- a/react-ui/src/pages/Experiment/Info/index.jsx +++ b/react-ui/src/pages/Experiment/Info/index.jsx @@ -28,6 +28,7 @@ function ExperimentText() { const [propsDrawerOpen, openPropsDrawer, closePropsDrawer, propsDrawerOpenRef] = useVisible(false); const navigate = useNavigate(); + const evtSourceRef = useRef(); const width = 110; const height = 36; @@ -48,6 +49,9 @@ function ExperimentText() { if (timerRef.current) { clearTimeout(timerRef.current); } + if (evtSourceRef.current) { + evtSourceRef.current.close(); + } }; }, []); @@ -68,7 +72,7 @@ function ExperimentText() { item.imgName = item.img.slice(0, item.img.length - 4); }); workflowRef.current = dag; - getExperimentInstance(true); + getExperimentInstance(); } catch (error) { // JSON.parse 错误 console.log(error); @@ -77,50 +81,30 @@ function ExperimentText() { }; // 获取实验实例 - const getExperimentInstance = async (first) => { + const getExperimentInstance = async () => { const [res] = await to(getExperimentIns(locationParams.id)); if (res && res.data && workflowRef.current) { setExperimentIns(res.data); - const { status, nodes_status } = res.data; + const { status, nodes_status, argo_ins_ns, argo_ins_name } = res.data; const workflowData = workflowRef.current; const experimentStatusObjs = JSON.parse(nodes_status); workflowData.nodes.forEach((item) => { - const experimentNode = experimentStatusObjs?.[item.id] ?? {}; - const { finishedAt, startedAt, phase, id } = experimentNode; - item.experimentStartTime = startedAt; - item.experimentEndTime = finishedAt; - item.experimentStatus = phase; - item.workflowId = id; - item.img = phase ? `${item.imgName}-${phase}.png` : `${item.imgName}.png`; + const experimentNode = experimentStatusObjs?.[item.id]; + updateWorkflowNode(item, experimentNode); }); - // 更新打开的抽屉数据 - if (propsDrawerOpenRef.current && experimentNodeDataRef.current) { - const currentId = experimentNodeDataRef.current.id; - const node = workflowData.nodes.find((item) => item.id === currentId); - if (node) { - setExperimentNodeData(node); - } - } - - getGraphData(workflowData, first); - - // 运行中或者等待中,每5秒获取一次实验实例 - if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { - timerRef.current = setTimeout(() => { - getExperimentInstance(false); - }, 5 * 1000); - } + // 绘制图 + getGraphData(workflowData, true); // 如果状态是 Pending, 打开第一个节点 // 如果状态是 Running,打开第一个运行中的节点,如果没有运行中的节点,打开第一个节点 - if (first && status === ExperimentStatus.Pending) { + if (status === ExperimentStatus.Pending) { const node = workflowData.nodes[0]; if (node) { setExperimentNodeData(node); openPropsDrawer(); } - } else if (first && status === ExperimentStatus.Running) { + } else if (status === ExperimentStatus.Running) { const node = workflowData.nodes.find((item) => item.experimentStatus === ExperimentStatus.Running) ?? workflowData.nodes[0]; @@ -129,9 +113,81 @@ function ExperimentText() { openPropsDrawer(); } } + + // 运行中或者等待中,开启 SSE + if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { + setupSSE(argo_ins_name, argo_ins_ns); + } } }; + const setupSSE = (name, namespace) => { + const { origin } = location; + const evtSource = new EventSource( + `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=metadata.namespace%3D${namespace}%2Cmetadata.name%3D${name}`, + { withCredentials: true }, + ); + evtSource.onmessage = (event) => { + const data = event?.data; + if (!data) { + return; + } + try { + const dataJson = JSON.parse(data); + const statusData = dataJson?.result?.object?.status; + if (!statusData) { + return; + } + const { startedAt, finishedAt, phase, nodes = {} } = statusData; + setExperimentIns((prev) => ({ + ...prev, + finish_time: finishedAt, + status: phase, + })); + + 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); + + // 更新打开的抽屉数据 + if (propsDrawerOpenRef.current && experimentNodeDataRef.current) { + const currentId = experimentNodeDataRef.current.id; + const node = workflowData.nodes.find((item) => item.id === currentId); + if (node) { + setExperimentNodeData(node); + } + } + if (phase !== ExperimentStatus.Pending && phase !== ExperimentStatus.Running) { + evtSource.close(); + } + } catch (error) { + console.log(error); + } + }; + evtSource.onerror = (error) => { + console.log('sse error', error); + }; + + evtSourceRef.current = evtSource; + }; + + function updateWorkflowNode(workflowNode, statusNode) { + if (!statusNode) { + return; + } + const { finishedAt, startedAt, phase, id } = statusNode; + workflowNode.experimentStartTime = startedAt; + workflowNode.experimentEndTime = finishedAt; + workflowNode.experimentStatus = phase; + workflowNode.workflowId = id; + workflowNode.img = phase + ? `${workflowNode.imgName}-${phase}.png` + : `${workflowNode.imgName}.png`; + } + // 根据数据,渲染图 const getGraphData = (data, first) => { if (graph) { @@ -151,7 +207,7 @@ function ExperimentText() { } } else { setTimeout(() => { - getGraphData(data); + getGraphData(data, first); }, 500); } }; diff --git a/react-ui/src/pages/Pipeline/editPipeline/index.jsx b/react-ui/src/pages/Pipeline/editPipeline/index.jsx index 7d0f4f8e..8a2aa24d 100644 --- a/react-ui/src/pages/Pipeline/editPipeline/index.jsx +++ b/react-ui/src/pages/Pipeline/editPipeline/index.jsx @@ -18,7 +18,7 @@ import { findAllParentNodes } from './utils'; let graph = null; const EditPipeline = () => { - const navgite = useNavigate(); + const navigate = useNavigate(); const locationParams = useParams(); //新版本获取路由参数接口 const graphRef = useRef(); const paramsDrawerRef = useRef(); @@ -104,9 +104,7 @@ const EditPipeline = () => { setTimeout(() => { const data = graph.save(); console.log(data); - const errorNode = data.nodes.find((item) => { - return item.formError === true; - }); + const errorNode = data.nodes.find((item) => item.formError === true); if (errorNode) { message.error(`【${errorNode.label}】节点必填项必须配置`); const graphNode = graph.findById(errorNode.id); @@ -124,7 +122,7 @@ const EditPipeline = () => { message.success('保存成功'); setTimeout(() => { if (val) { - navgite({ pathname: `/pipeline/template` }); + navigate({ pathname: `/pipeline/template` }); } }, 500); }); From 02a1f2ec8b0af20b8afa4f641e722b9181ddca41 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Wed, 10 Jul 2024 10:52:03 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E5=AE=9E=E9=AA=8C=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=E6=97=A5=E5=BF=97=E6=94=B9=E4=B8=BA=20websocket=20?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/pages/Experiment/Info/index.jsx | 24 ++- react-ui/src/pages/Experiment/Info/props.tsx | 139 ------------------ .../ExperimentDrawer/index.less} | 0 .../components/ExperimentDrawer/index.tsx | 131 +++++++++++++++++ .../components/ExperimentResult/index.tsx | 4 +- .../Experiment/components/LogGroup/index.tsx | 104 ++++++++++--- .../Experiment/components/LogList/index.tsx | 46 ++++-- 7 files changed, 266 insertions(+), 182 deletions(-) delete mode 100644 react-ui/src/pages/Experiment/Info/props.tsx rename react-ui/src/pages/Experiment/{Info/props.less => components/ExperimentDrawer/index.less} (100%) create mode 100644 react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx diff --git a/react-ui/src/pages/Experiment/Info/index.jsx b/react-ui/src/pages/Experiment/Info/index.jsx index f2a5fd6c..d0fc48e9 100644 --- a/react-ui/src/pages/Experiment/Info/index.jsx +++ b/react-ui/src/pages/Experiment/Info/index.jsx @@ -10,10 +10,10 @@ import G6, { Util } from '@antv/g6'; import { Button } from 'antd'; import { useEffect, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; +import ExperimentDrawer from '../components/ExperimentDrawer'; import ParamsModal from '../components/ViewParamsModal'; import { experimentStatusInfo } from '../status'; import styles from './index.less'; -import ExperimentDrawer from './props'; let graph = null; @@ -51,6 +51,7 @@ function ExperimentText() { } if (evtSourceRef.current) { evtSourceRef.current.close(); + evtSourceRef.current = null; } }; }, []); @@ -123,9 +124,10 @@ function ExperimentText() { const setupSSE = (name, namespace) => { const { origin } = location; + const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); const evtSource = new EventSource( - `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=metadata.namespace%3D${namespace}%2Cmetadata.name%3D${name}`, - { withCredentials: true }, + `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, + { withCredentials: false }, ); evtSource.onmessage = (event) => { const data = event?.data; @@ -162,6 +164,7 @@ function ExperimentText() { } if (phase !== ExperimentStatus.Pending && phase !== ExperimentStatus.Running) { evtSource.close(); + evtSourceRef.current = null; } } catch (error) { console.log(error); @@ -448,6 +451,10 @@ function ExperimentText() { graph.on('node:mouseleave', (e) => { graph.setItemState(e.item, 'hover', false); }); + graph.on('canvas:click', (e) => { + setExperimentNodeData(null); + closePropsDrawer(); + }); }; return ( @@ -483,18 +490,19 @@ function ExperimentText() {
- {experimentNodeData ? ( + {experimentIns && experimentNodeData ? ( ) : null} void; - instanceId?: number; // 实验实例 id - instanceName?: string; // 实验实例 name - instanceNamespace?: string; // 实验实例 namespace - instanceNodeData: PipelineNodeModelSerialize; // 节点数据,在定时刷新实验实例状态中不会变化 - workflowId?: string; // 实验实例工作流 id - instanceNodeStatus?: ExperimentStatus; // 实例节点状态 - instanceNodeStartTime?: string; // 开始时间 - instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化 -}; - -const ExperimentDrawer = forwardRef( - ( - { - open, - onClose, - instanceId, - instanceName, - instanceNamespace, - instanceNodeData, - workflowId, - instanceNodeStatus, - instanceNodeStartTime, - instanceNodeEndTime, - }: ExperimentDrawerProps, - ref, - ) => { - useImperativeHandle(ref, () => ({})); - - // 如果性能有问题,可以进一步拆解 - const items = useMemo( - () => [ - { - key: '1', - label: '日志详情', - children: ( - - ), - icon: , - }, - { - key: '2', - label: '配置参数', - icon: , - children: , - }, - { - key: '3', - label: '输出结果', - children: ( - - ), - icon: , - }, - ], - [ - instanceNodeData, - instanceId, - instanceName, - instanceNamespace, - instanceNodeStatus, - workflowId, - instanceNodeStartTime, - ], - ); - - return ( - -
-
- 任务名称:{instanceNodeData.label} -
-
- 执行状态: - {instanceNodeStatus ? ( - <> -
- - {experimentStatusInfo[instanceNodeStatus]?.label} - - - ) : ( - '--' - )} -
-
- 启动时间:{formatDate(instanceNodeStartTime)} -
-
- 耗时: - {elapsedTime(instanceNodeStartTime, instanceNodeEndTime)} -
-
- -
- ); - }, -); - -export default ExperimentDrawer; diff --git a/react-ui/src/pages/Experiment/Info/props.less b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less similarity index 100% rename from react-ui/src/pages/Experiment/Info/props.less rename to react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less diff --git a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx new file mode 100644 index 00000000..f93950fa --- /dev/null +++ b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx @@ -0,0 +1,131 @@ +import { ExperimentStatus } from '@/enums'; +import { experimentStatusInfo } from '@/pages/Experiment/status'; +import { PipelineNodeModelSerialize } from '@/types'; +import { elapsedTime, formatDate } from '@/utils/date'; +import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; +import { Drawer, Tabs } from 'antd'; +import { useMemo } from 'react'; +import ExperimentParameter from '../ExperimentParameter'; +import ExperimentResult from '../ExperimentResult'; +import LogList from '../LogList'; +import styles from './index.less'; + +type ExperimentDrawerProps = { + open: boolean; + onClose: () => void; + instanceId: number; // 实验实例 id + instanceName: string; // 实验实例 name + instanceNamespace: string; // 实验实例 namespace + instanceNodeData: PipelineNodeModelSerialize; // 节点数据,在定时刷新实验实例状态中不会变化 + workflowId?: string; // 实验实例工作流 id + instanceNodeStatus?: ExperimentStatus; // 实例节点状态 + instanceNodeStartTime?: string; // 开始时间 + instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化 +}; + +const ExperimentDrawer = ({ + open, + onClose, + instanceId, + instanceName, + instanceNamespace, + instanceNodeData, + workflowId, + instanceNodeStatus, + instanceNodeStartTime, + instanceNodeEndTime, +}: ExperimentDrawerProps) => { + // 如果性能有问题,可以进一步拆解 + const items = useMemo( + () => [ + { + key: '1', + label: '日志详情', + children: ( + + ), + icon: , + }, + { + key: '2', + label: '配置参数', + icon: , + children: , + }, + { + key: '3', + label: '输出结果', + children: ( + + ), + icon: , + }, + ], + [ + instanceNodeData, + instanceId, + instanceName, + instanceNamespace, + instanceNodeStatus, + workflowId, + instanceNodeStartTime, + ], + ); + + return ( + +
+
任务名称:{instanceNodeData.label}
+
+ 执行状态: + {instanceNodeStatus ? ( + <> +
+ + {experimentStatusInfo[instanceNodeStatus]?.label} + + + ) : ( + '--' + )} +
+
+ 启动时间:{formatDate(instanceNodeStartTime)} +
+
+ 耗时: + {elapsedTime(instanceNodeStartTime, instanceNodeEndTime)} +
+
+ +
+ ); +}; + +export default ExperimentDrawer; diff --git a/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx index feabcc3d..046c2afe 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx @@ -8,8 +8,8 @@ import ExportModelModal from '../ExportModelModal'; import styles from './index.less'; type ExperimentResultProps = { - experimentInsId?: number; // 实验实例 id - pipelineNodeId?: string; // 流水线节点 id + experimentInsId: number; // 实验实例 id + pipelineNodeId: string; // 流水线节点 id }; type ExperimentResultData = { diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx index 4440c9cb..38e972b2 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx @@ -10,6 +10,7 @@ import { getExperimentPodsLog } from '@/services/experiment/index.js'; import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons'; import { Button } from 'antd'; import classNames from 'classnames'; +import dayjs from 'dayjs'; import { useEffect, useRef, useState } from 'react'; import { ExperimentLog } from '../LogList'; import styles from './index.less'; @@ -21,6 +22,7 @@ export type LogGroupProps = ExperimentLog & { type Log = { start_time: string; // 日志开始时间 log_content: string; // 日志内容 + pod_name: string; // pod名称 }; // 滚动到底部 @@ -49,44 +51,33 @@ function LogGroup({ // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); const preStatusRef = useRef(undefined); + const socketRef = useRef(undefined); + const retryRef = useRef(2); useEffect(() => { scrollToBottom(false); - let timerId: NodeJS.Timeout | undefined; if (status === ExperimentStatus.Running) { - timerId = setInterval(() => { - requestExperimentPodsLog(); - }, 5 * 1000); + setupSockect(); } else if (preStatusRef.current === ExperimentStatus.Running) { - requestExperimentPodsLog(); - setTimeout(() => { - requestExperimentPodsLog(); - }, 5 * 1000); + setCompleted(true); } preStatusRef.current = status; - return () => { - if (timerId) { - clearInterval(timerId); - timerId = undefined; - } - }; }, [status]); + // 鼠标拖到中不滚动到底部 useEffect(() => { const mouseDown = () => { setIsMouseDown(true); }; - const mouseUp = () => { setIsMouseDown(false); }; - document.addEventListener('mousedown', mouseDown); document.addEventListener('mouseup', mouseUp); - return () => { document.removeEventListener('mousedown', mouseDown); document.removeEventListener('mouseup', mouseUp); + closeSocket(); }; }, []); @@ -140,6 +131,85 @@ function LogGroup({ requestExperimentPodsLog(); }; + // 建立 socket 连接 + const setupSockect = () => { + let { host } = location; + console.log('setupSockect'); + + if (process.env.NODE_ENV === 'development') { + host = '172.20.32.181:31213'; + } + const socket = new WebSocket( + `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, + ); + + socket.addEventListener('open', () => { + console.log('WebSocket is open now.'); + }); + + socket.addEventListener('close', (event) => { + console.log('WebSocket is closed:', event); + if (event.code !== 1000 && retryRef.current > 0) { + retryRef.current -= 1; + setTimeout(() => { + setupSockect(); + }, 2 * 1000); + } + }); + + socket.addEventListener('error', (event) => { + console.error('WebSocket error observed:', event); + }); + + socket.addEventListener('message', (event) => { + if (!event.data) { + return; + } + try { + const data = JSON.parse(event.data); + const streams = data.streams; + if (!streams || !Array.isArray(streams)) { + return; + } + let startTime = start_time; + const logContent = streams.reduce((result, item) => { + const values = item.values; + return ( + result + + values.reduce((prev: string, cur: [string, string]) => { + const [time, value] = cur; + startTime = time; + const str = `[${dayjs(Number(time) / 1.0e6).format('YYYY-MM-DD HH:mm:ss')}] ${value}`; + return prev + str; + }, '') + ); + }, ''); + const logDetail: Log = { + start_time: startTime!, + log_content: logContent, + pod_name: pod_name, + }; + setLogList((oldList) => oldList.concat(logDetail)); + if (!isMouseDownRef.current && logContent) { + setTimeout(() => { + scrollToBottom(); + }, 100); + } + } catch (error) { + console.log(error); + } + }); + + socketRef.current = socket; + }; + + const closeSocket = () => { + if (socketRef.current) { + socketRef.current.close(1000, 'completed'); + socketRef.current = undefined; + } + }; + const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; const logText = log_content + logList.map((v) => v.log_content).join(''); const showMoreBtn = diff --git a/react-ui/src/pages/Experiment/components/LogList/index.tsx b/react-ui/src/pages/Experiment/components/LogList/index.tsx index 911bf5d9..2bf94631 100644 --- a/react-ui/src/pages/Experiment/components/LogList/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogList/index.tsx @@ -2,7 +2,7 @@ import { ExperimentStatus } from '@/enums'; import { getQueryByExperimentLog } from '@/services/experiment/index.js'; import { to } from '@/utils/promise'; import dayjs from 'dayjs'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import LogGroup from '../LogGroup'; import styles from './index.less'; @@ -14,9 +14,9 @@ export type ExperimentLog = { }; type LogListProps = { - instanceName?: string; // 实验实例 name - instanceNamespace?: string; // 实验实例 namespace - pipelineNodeId?: string; // 流水线节点 id + instanceName: string; // 实验实例 name + instanceNamespace: string; // 实验实例 namespace + pipelineNodeId: string; // 流水线节点 id workflowId?: string; // 实验实例工作流 id instanceNodeStartTime?: string; // 实验实例节点开始运行时间 instanceNodeStatus?: ExperimentStatus; @@ -31,23 +31,30 @@ function LogList({ instanceNodeStatus, }: LogListProps) { const [logList, setLogList] = useState([]); + const preStatusRef = useRef(undefined); + const retryRef = useRef(3); useEffect(() => { - if (workflowId) { - const start_time = dayjs(instanceNodeStartTime).valueOf() * 1.0e6; - const params = { - task_id: pipelineNodeId, - component_id: workflowId, - name: instanceName, - namespace: instanceNamespace, - start_time: start_time, - }; - getExperimentLog(params, start_time); + if ( + instanceNodeStatus && + instanceNodeStatus !== ExperimentStatus.Pending && + (!preStatusRef.current || preStatusRef.current === ExperimentStatus.Pending) + ) { + getExperimentLog(); } - }, [workflowId, instanceNodeStartTime]); + preStatusRef.current = instanceNodeStatus; + }, [instanceNodeStatus]); // 获取实验日志 - const getExperimentLog = async (params: any, start_time: number) => { + const getExperimentLog = async () => { + const start_time = dayjs(instanceNodeStartTime).valueOf() * 1.0e6; + const params = { + task_id: pipelineNodeId, + component_id: workflowId, + name: instanceName, + namespace: instanceNamespace, + start_time: start_time, + }; const [res] = await to(getQueryByExperimentLog(params)); if (res && res.data) { const { log_type, pods, log_detail } = res.data; @@ -68,6 +75,13 @@ function LogList({ })); setLogList(list); } + } else { + if (retryRef.current > 0) { + retryRef.current -= 1; + setTimeout(() => { + getExperimentLog(); + }, 2 * 1000); + } } };