Merge pull request '合并dev' (#102) from dev into master
This commit is contained in:
commit
6b39f6d295
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -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,10 @@ function ExperimentText() {
|
|||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
if (evtSourceRef.current) {
|
||||
evtSourceRef.current.close();
|
||||
evtSourceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
@ -68,7 +73,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,48 +82,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, true);
|
||||
|
||||
getGraphData(workflowData, first);
|
||||
|
||||
// 运行中或者等待中,每5秒获取一次实验实例
|
||||
if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) {
|
||||
timerRef.current = setTimeout(() => {
|
||||
getExperimentInstance(false);
|
||||
}, 5 * 1000);
|
||||
}
|
||||
|
||||
if (first && status === ExperimentStatus.Pending) {
|
||||
// 如果状态是 Pending, 打开第一个节点
|
||||
// 如果状态是 Running,打开第一个运行中的节点,如果没有运行中的节点,打开第一个节点
|
||||
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];
|
||||
|
@ -127,9 +114,83 @@ 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 params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`);
|
||||
const evtSource = new EventSource(
|
||||
`${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`,
|
||||
{ withCredentials: false },
|
||||
);
|
||||
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();
|
||||
evtSourceRef.current = null;
|
||||
}
|
||||
} 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) {
|
||||
|
@ -149,7 +210,7 @@ function ExperimentText() {
|
|||
}
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
getGraphData(data);
|
||||
getGraphData(data, first);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
@ -390,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 (
|
||||
|
@ -425,18 +490,19 @@ function ExperimentText() {
|
|||
</Button>
|
||||
</div>
|
||||
<div className={styles['pipeline-container__graph']} ref={graphRef}></div>
|
||||
{experimentNodeData ? (
|
||||
{experimentIns && experimentNodeData ? (
|
||||
<ExperimentDrawer
|
||||
key={experimentNodeData.id}
|
||||
open={propsDrawerOpen}
|
||||
onClose={closePropsDrawer}
|
||||
instanceId={experimentIns?.id}
|
||||
instanceName={experimentIns?.argo_ins_name}
|
||||
instanceNamespace={experimentIns?.argo_ins_ns}
|
||||
instanceId={experimentIns.id}
|
||||
instanceName={experimentIns.argo_ins_name}
|
||||
instanceNamespace={experimentIns.argo_ins_ns}
|
||||
instanceNodeData={experimentNodeData}
|
||||
workflowId={experimentNodeData.workflowId}
|
||||
instanceNodeStatus={experimentNodeData.experimentStatus}
|
||||
instanceNodeStartTime={experimentNodeData.experimentStartTime}
|
||||
instanceNodeEndTime={experimentIns.experimentEndTime}
|
||||
instanceNodeEndTime={experimentNodeData.experimentEndTime}
|
||||
></ExperimentDrawer>
|
||||
) : null}
|
||||
<ParamsModal
|
||||
|
|
|
@ -1,146 +0,0 @@
|
|||
import { ExperimentStatus } from '@/enums';
|
||||
import { PipelineNodeModelSerialize } from '@/types';
|
||||
import { elapsedTime, formatDate } from '@/utils/date';
|
||||
import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons';
|
||||
import { Drawer, Tabs } from 'antd';
|
||||
import { forwardRef, useImperativeHandle, useMemo } from 'react';
|
||||
import ExperimentParameter from '../components/ExperimentParameter';
|
||||
import ExperimentResult from '../components/ExperimentResult';
|
||||
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;
|
||||
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: (
|
||||
<LogList
|
||||
instanceName={instanceName}
|
||||
instanceNamespace={instanceNamespace}
|
||||
pipelineNodeId={instanceNodeData.id}
|
||||
workflowId={workflowId}
|
||||
instanceNodeStartTime={instanceNodeStartTime}
|
||||
instanceNodeStatus={instanceNodeStatus}
|
||||
></LogList>
|
||||
),
|
||||
icon: <ProfileOutlined />,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: '配置参数',
|
||||
icon: <DatabaseOutlined />,
|
||||
children: <ExperimentParameter nodeData={instanceNodeData} />,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: '输出结果',
|
||||
children: (
|
||||
<ExperimentResult
|
||||
experimentInsId={instanceId}
|
||||
pipelineNodeId={instanceNodeData.id}
|
||||
></ExperimentResult>
|
||||
),
|
||||
icon: <ProfileOutlined />,
|
||||
},
|
||||
],
|
||||
[
|
||||
instanceNodeData,
|
||||
instanceId,
|
||||
instanceName,
|
||||
instanceNamespace,
|
||||
instanceNodeStatus,
|
||||
workflowId,
|
||||
instanceNodeStartTime,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title="任务执行详情"
|
||||
placement="right"
|
||||
getContainer={false}
|
||||
closeIcon={false}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
width={520}
|
||||
className={styles['experiment-drawer']}
|
||||
destroyOnClose={true}
|
||||
>
|
||||
<div style={{ paddingTop: '15px' }}>
|
||||
<div className={styles['experiment-drawer__info']}>
|
||||
任务名称:{instanceNodeData.label}
|
||||
</div>
|
||||
<div className={styles['experiment-drawer__info']}>
|
||||
执行状态:
|
||||
{instanceNodeStatus ? (
|
||||
<>
|
||||
<div
|
||||
className={styles['experiment-drawer__status-dot']}
|
||||
style={{
|
||||
backgroundColor: experimentStatusInfo[instanceNodeStatus]?.color,
|
||||
}}
|
||||
></div>
|
||||
<span style={{ color: experimentStatusInfo[instanceNodeStatus]?.color }}>
|
||||
{experimentStatusInfo[instanceNodeStatus]?.label}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
'--'
|
||||
)}
|
||||
</div>
|
||||
<div className={styles['experiment-drawer__info']}>
|
||||
启动时间:{formatDate(instanceNodeStartTime)}
|
||||
</div>
|
||||
<div className={styles['experiment-drawer__info']}>
|
||||
耗时:
|
||||
{elapsedTime(instanceNodeStartTime, instanceNodeEndTime)}
|
||||
</div>
|
||||
</div>
|
||||
<Tabs defaultActiveKey="1" items={items} className={styles['experiment-drawer__tabs']} />
|
||||
</Drawer>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default ExperimentDrawer;
|
|
@ -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: (
|
||||
<LogList
|
||||
instanceName={instanceName}
|
||||
instanceNamespace={instanceNamespace}
|
||||
pipelineNodeId={instanceNodeData.id}
|
||||
workflowId={workflowId}
|
||||
instanceNodeStartTime={instanceNodeStartTime}
|
||||
instanceNodeStatus={instanceNodeStatus}
|
||||
></LogList>
|
||||
),
|
||||
icon: <ProfileOutlined />,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: '配置参数',
|
||||
icon: <DatabaseOutlined />,
|
||||
children: <ExperimentParameter nodeData={instanceNodeData} />,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: '输出结果',
|
||||
children: (
|
||||
<ExperimentResult
|
||||
experimentInsId={instanceId}
|
||||
pipelineNodeId={instanceNodeData.id}
|
||||
></ExperimentResult>
|
||||
),
|
||||
icon: <ProfileOutlined />,
|
||||
},
|
||||
],
|
||||
[
|
||||
instanceNodeData,
|
||||
instanceId,
|
||||
instanceName,
|
||||
instanceNamespace,
|
||||
instanceNodeStatus,
|
||||
workflowId,
|
||||
instanceNodeStartTime,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title="任务执行详情"
|
||||
placement="right"
|
||||
getContainer={false}
|
||||
closeIcon={false}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
width={520}
|
||||
className={styles['experiment-drawer']}
|
||||
destroyOnClose={true}
|
||||
mask={false}
|
||||
>
|
||||
<div style={{ paddingTop: '15px' }}>
|
||||
<div className={styles['experiment-drawer__info']}>任务名称:{instanceNodeData.label}</div>
|
||||
<div className={styles['experiment-drawer__info']}>
|
||||
执行状态:
|
||||
{instanceNodeStatus ? (
|
||||
<>
|
||||
<div
|
||||
className={styles['experiment-drawer__status-dot']}
|
||||
style={{
|
||||
backgroundColor: experimentStatusInfo[instanceNodeStatus]?.color,
|
||||
}}
|
||||
></div>
|
||||
<span style={{ color: experimentStatusInfo[instanceNodeStatus]?.color }}>
|
||||
{experimentStatusInfo[instanceNodeStatus]?.label}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
'--'
|
||||
)}
|
||||
</div>
|
||||
<div className={styles['experiment-drawer__info']}>
|
||||
启动时间:{formatDate(instanceNodeStartTime)}
|
||||
</div>
|
||||
<div className={styles['experiment-drawer__info']}>
|
||||
耗时:
|
||||
{elapsedTime(instanceNodeStartTime, instanceNodeEndTime)}
|
||||
</div>
|
||||
</div>
|
||||
<Tabs defaultActiveKey="1" items={items} className={styles['experiment-drawer__tabs']} />
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExperimentDrawer;
|
|
@ -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 = {
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
|
||||
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 dayjs from 'dayjs';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { ExperimentLog } from '../LogList';
|
||||
import styles from './index.less';
|
||||
|
||||
export type LogGroupProps = ExperimentLog & {
|
||||
|
@ -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<ExperimentStatus | undefined>(undefined);
|
||||
const socketRef = useRef<WebSocket | undefined>(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 =
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
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';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, 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
|
||||
pipelineNodeId?: string; // 流水线节点 id
|
||||
instanceName: string; // 实验实例 name
|
||||
instanceNamespace: string; // 实验实例 namespace
|
||||
pipelineNodeId: string; // 流水线节点 id
|
||||
workflowId?: string; // 实验实例工作流 id
|
||||
instanceNodeStartTime?: string; // 实验实例节点开始运行时间
|
||||
instanceNodeStatus?: ExperimentStatus;
|
||||
|
@ -25,23 +31,30 @@ function LogList({
|
|||
instanceNodeStatus,
|
||||
}: LogListProps) {
|
||||
const [logList, setLogList] = useState<ExperimentLog[]>([]);
|
||||
const preStatusRef = useRef<ExperimentStatus | undefined>(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;
|
||||
|
@ -62,6 +75,13 @@ function LogList({
|
|||
}));
|
||||
setLogList(list);
|
||||
}
|
||||
} else {
|
||||
if (retryRef.current > 0) {
|
||||
retryRef.current -= 1;
|
||||
setTimeout(() => {
|
||||
getExperimentLog();
|
||||
}, 2 * 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -31,7 +31,7 @@ public interface ExperimentInsService {
|
|||
/**
|
||||
* 根据实验ID查找所有具有相同ID的实例,并将它们添加到实验列表中
|
||||
*
|
||||
* @param experimentId 实验id,不是实例id
|
||||
* @param experimentId 实验id,不是实验实例id
|
||||
* @return 实例列表
|
||||
*/
|
||||
List<ExperimentIns> getByExperimentId(Integer experimentId) throws IOException;
|
||||
|
|
|
@ -341,6 +341,9 @@ public class ImageServiceImpl implements ImageService {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Map<String, String> uploadImageFiles(MultipartFile file) throws Exception {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
|
|
|
@ -99,10 +99,7 @@ public class JupyterServiceImpl implements JupyterService {
|
|||
|
||||
// 调用修改后的 createPod 方法,传入额外的参数
|
||||
Integer podPort = k8sClientUtil.createConfiguredPod(podName, namespace, port, mountPath, pvc, image, minioPvcName, datasetPath, modelPath);
|
||||
// // 简单的延迟,以便 Pod 有时间启动
|
||||
// Thread.sleep(2500);
|
||||
// //查询pod状态,更新到数据库
|
||||
// String podStatus = k8sClientUtil.getPodStatus(podName, namespace);
|
||||
|
||||
String url = masterIp + ":" + podPort;
|
||||
redisService.setCacheObject(podName,masterIp + ":" + podPort);
|
||||
devEnvironment.setStatus("Pending");
|
||||
|
@ -129,7 +126,6 @@ public class JupyterServiceImpl implements JupyterService {
|
|||
// 使用 Kubernetes API 删除 Pod
|
||||
String deleteResult = k8sClientUtil.deletePod(podName, namespace);
|
||||
|
||||
|
||||
devEnvironment.setStatus("Terminated");
|
||||
this.devEnvironmentService.update(devEnvironment);
|
||||
return deleteResult + ",编辑器已停止";
|
||||
|
|
|
@ -19,58 +19,90 @@ public class AIM64EncoderUtil {
|
|||
}
|
||||
|
||||
public static String aim64encode(Map<String, Object> value) {
|
||||
// 将Map对象转换为JSON字符串
|
||||
String jsonEncoded = JSON.toJSONString(value);
|
||||
// 将JSON字符串进行Base64编码
|
||||
String base64Encoded = Base64.getEncoder().encodeToString(jsonEncoded.getBytes());
|
||||
String aim64Encoded = base64Encoded;
|
||||
// 替换Base64编码中的特定字符
|
||||
for (Map.Entry<String, String> entry : BS64_REPLACE_CHARACTERS_ENCODING.entrySet()) {
|
||||
aim64Encoded = aim64Encoded.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
// 返回带有前缀的AIM64编码字符串
|
||||
return AIM64_ENCODING_PREFIX + aim64Encoded;
|
||||
}
|
||||
|
||||
|
||||
// 这是一个静态方法,用于将 Map 对象 value 编码为字符串
|
||||
public static String encode(Map<String, Object> value, boolean oneWayHashing) {
|
||||
// 如果 oneWayHashing 参数为 true,表示使用一种不可逆的哈希算法
|
||||
if (oneWayHashing) {
|
||||
// 使用 MD5 算法将 value 对象转换为字符串
|
||||
return md5(JSON.toJSONString(value));
|
||||
}
|
||||
// 如果 oneWayHashing 参数为 false,表示使用一种可逆的编码算法
|
||||
return aim64encode(value);
|
||||
}
|
||||
|
||||
|
||||
// MD5 加密函数,用于将输入字符串转换为 MD5 加密后的字符串。
|
||||
private static String md5(String input) {
|
||||
// 尝试获取 MD5 加密对象
|
||||
try {
|
||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
|
||||
// 使用 MD5 对象对输入字符串进行加密
|
||||
byte[] array = md.digest(input.getBytes());
|
||||
// 创建一个 StringBuilder 对象,用于将加密后的字节转换为字符串
|
||||
StringBuilder sb = new StringBuilder();
|
||||
// 遍历加密后的字节数组
|
||||
for (byte b : array) {
|
||||
// 将每个字节转换为 16 进制字符串
|
||||
sb.append(Integer.toHexString((b & 0xFF) | 0x100).substring(1, 3));
|
||||
}
|
||||
// 返回加密后的字符串
|
||||
return sb.toString();
|
||||
} catch (java.security.NoSuchAlgorithmException e) {
|
||||
// 如果在获取 MD5 加密对象时出现了错误,则打印错误信息
|
||||
e.printStackTrace();
|
||||
}
|
||||
// 如果出现了错误,则返回 null
|
||||
return null;
|
||||
}
|
||||
|
||||
// 这是一个 decode 方法,用于将 runIds 列表转换为查询字符串
|
||||
public static String decode(List<String> runIds) {
|
||||
// 确保 runIds 列表的大小为 3
|
||||
if (runIds == null || runIds.size() == 0) {
|
||||
// 如果 runIds 为空,抛出 IllegalArgumentException 异常
|
||||
throw new IllegalArgumentException("runIds 不能为空");
|
||||
}
|
||||
// 构建查询字符串
|
||||
StringBuilder queryBuilder = new StringBuilder("run.hash in [");
|
||||
// 遍历 runIds 列表
|
||||
for (int i = 0; i < runIds.size(); i++) {
|
||||
// 如果当前不是第一个元素,添加逗号
|
||||
if (i > 0) {
|
||||
queryBuilder.append(",");
|
||||
}
|
||||
// 将当前元素添加到查询字符串中,使用双引号括起来
|
||||
queryBuilder.append("\"").append(runIds.get(i)).append("\"");
|
||||
}
|
||||
// 将查询字符串闭合
|
||||
queryBuilder.append("]");
|
||||
// 将查询字符串转换为字符串
|
||||
String query = queryBuilder.toString();
|
||||
// 创建一个 Map 对象,用于存储查询参数
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
// 将查询字符串添加到 Map 中
|
||||
map.put("query", query);
|
||||
// 将 advancedMode 设置为 true
|
||||
map.put("advancedMode", true);
|
||||
// 将 advancedQuery 设置为查询字符串
|
||||
map.put("advancedQuery", query);
|
||||
|
||||
// 使用 encode 方法将 Map 对象转换为查询字符串
|
||||
String searchQuery = encode(map, false);
|
||||
// 返回查询字符串
|
||||
return searchQuery;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,19 +9,30 @@ import java.util.Set;
|
|||
|
||||
public class BeansUtils {
|
||||
|
||||
// 获取一个 Java 对象中所有为空的属性名称。:
|
||||
public static String[] getNullPropertyNames(Object source) {
|
||||
// 创建一个 BeanWrapper 对象,参数为给定的 Java 对象
|
||||
final BeanWrapper src = new BeanWrapperImpl(source);
|
||||
// 获取该对象所有属性描述符
|
||||
java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors();
|
||||
// 创建一个集合,用于存储为空的属性名称
|
||||
Set<String> emptyNames = new HashSet<String>();
|
||||
// 遍历所有属性描述符
|
||||
for(java.beans.PropertyDescriptor pd : pds) {
|
||||
// 获取属性值
|
||||
Object srcValue = src.getPropertyValue(pd.getName());
|
||||
// 如果属性值为空,则将属性名称添加到集合中
|
||||
if (srcValue == null) emptyNames.add(pd.getName());
|
||||
}
|
||||
// 创建一个字符串数组,用于存储为空的属性名称
|
||||
String[] result = new String[emptyNames.size()];
|
||||
// 将为空的属性名称存储到字符串数组中
|
||||
return emptyNames.toArray(result);
|
||||
}
|
||||
|
||||
|
||||
public static void copyPropertiesIgnoreNull(Object src, Object target){
|
||||
// 使用 BeanUtils 工具类将源对象中的非空属性复制到目标对象中
|
||||
BeanUtils.copyProperties(src, target, getNullPropertyNames(src));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,13 @@ import java.util.List;
|
|||
public class ConvertUtil {
|
||||
public static final Logger logger = LoggerFactory.getLogger(ConvertUtil.class);
|
||||
|
||||
|
||||
/**
|
||||
* 将实体对象转换为VO对象
|
||||
* @param source 源实体对象
|
||||
* @param target 目标VO对象的类
|
||||
* @return 转换后的VO对象
|
||||
*/
|
||||
public static <T> T entityToVo(Object source, Class<T> target) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
|
@ -33,6 +40,8 @@ public class ConvertUtil {
|
|||
return targetObject;
|
||||
}
|
||||
|
||||
|
||||
// 将实体对象列表转换为目标类型的列表
|
||||
public static <T> List<T> entityToVoList(Collection<?> sourceList, Class<T> target) {
|
||||
if (sourceList == null) {
|
||||
return null;
|
||||
|
@ -50,5 +59,6 @@ public class ConvertUtil {
|
|||
}
|
||||
return targetList;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -2,20 +2,29 @@ package com.ruoyi.platform.utils;
|
|||
|
||||
public class FileUtil {
|
||||
|
||||
// 格式化文件大小为可读的字符串表示
|
||||
public static String formatFileSize(long sizeInBytes) {
|
||||
// 检查文件大小是否为负数
|
||||
if (sizeInBytes < 0) {
|
||||
throw new IllegalArgumentException("File size cannot be negative.");
|
||||
}
|
||||
|
||||
// 如果文件大小小于1KB,直接返回字节数
|
||||
if (sizeInBytes < 1024) {
|
||||
return sizeInBytes + " B";
|
||||
} else if (sizeInBytes < 1024 * 1024) {
|
||||
}
|
||||
// 如果文件大小小于1MB,转换为KB并返回
|
||||
else if (sizeInBytes < 1024 * 1024) {
|
||||
double sizeInKB = sizeInBytes / 1024.0;
|
||||
return String.format("%.2f KB", sizeInKB);
|
||||
} else if (sizeInBytes < 1024 * 1024 * 1024) {
|
||||
}
|
||||
// 如果文件大小小于1GB,转换为MB并返回
|
||||
else if (sizeInBytes < 1024 * 1024 * 1024) {
|
||||
double sizeInMB = sizeInBytes / (1024.0 * 1024);
|
||||
return String.format("%.2f MB", sizeInMB);
|
||||
} else {
|
||||
}
|
||||
// 如果文件大小大于或等于1GB,转换为GB并返回
|
||||
else {
|
||||
double sizeInGB = sizeInBytes / (1024.0 * 1024 * 1024);
|
||||
return String.format("%.2f GB", sizeInGB);
|
||||
}
|
||||
|
|
|
@ -37,10 +37,8 @@ public class JsonUtils {
|
|||
// 将JSON字符串转换为扁平化的Map
|
||||
public static Map<String, Object> flattenJson(String prefix, Map<String, Object> map) {
|
||||
Map<String, Object> flatMap = new HashMap<>();
|
||||
Iterator<Map.Entry<String, Object>> entries = map.entrySet().iterator();
|
||||
|
||||
while (entries.hasNext()) {
|
||||
Map.Entry<String, Object> entry = entries.next();
|
||||
for (Map.Entry<String, Object> entry : map.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
|
||||
|
|
|
@ -371,7 +371,7 @@ public class K8sClientUtil {
|
|||
return service.getSpec().getPorts().get(0).getNodePort();
|
||||
}
|
||||
|
||||
|
||||
// 创建配置好的Pod
|
||||
public Integer createConfiguredPod(String podName, String namespace, Integer port, String mountPath, V1PersistentVolumeClaim pvc, String image, String dataPvcName, String datasetPath, String modelPath) {
|
||||
Map<String, String> selector = new LinkedHashMap<>();
|
||||
selector.put("k8s-jupyter", podName);
|
||||
|
|
Loading…
Reference in New Issue