From 02a1f2ec8b0af20b8afa4f641e722b9181ddca41 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Wed, 10 Jul 2024 10:52:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E9=AA=8C=E5=AE=9E=E4=BE=8B?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=94=B9=E4=B8=BA=20websocket=20=E8=BF=9E?= =?UTF-8?q?=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); + } } };