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);
+ }
}
};