| @@ -32,7 +32,7 @@ | |||||
| "record": "cross-env NODE_ENV=development REACT_APP_ENV=test max record --scene=login", | "record": "cross-env NODE_ENV=development REACT_APP_ENV=test max record --scene=login", | ||||
| "serve": "umi-serve", | "serve": "umi-serve", | ||||
| "start": "cross-env UMI_ENV=dev max dev", | "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: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: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", | "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 { Button } from 'antd'; | ||||
| import { useEffect, useRef, useState } from 'react'; | import { useEffect, useRef, useState } from 'react'; | ||||
| import { useNavigate, useParams } from 'react-router-dom'; | import { useNavigate, useParams } from 'react-router-dom'; | ||||
| import ExperimentDrawer from '../components/ExperimentDrawer'; | |||||
| import ParamsModal from '../components/ViewParamsModal'; | import ParamsModal from '../components/ViewParamsModal'; | ||||
| import { experimentStatusInfo } from '../status'; | import { experimentStatusInfo } from '../status'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import ExperimentDrawer from './props'; | |||||
| let graph = null; | let graph = null; | ||||
| @@ -28,6 +28,7 @@ function ExperimentText() { | |||||
| const [propsDrawerOpen, openPropsDrawer, closePropsDrawer, propsDrawerOpenRef] = | const [propsDrawerOpen, openPropsDrawer, closePropsDrawer, propsDrawerOpenRef] = | ||||
| useVisible(false); | useVisible(false); | ||||
| const navigate = useNavigate(); | const navigate = useNavigate(); | ||||
| const evtSourceRef = useRef(); | |||||
| const width = 110; | const width = 110; | ||||
| const height = 36; | const height = 36; | ||||
| @@ -48,6 +49,10 @@ function ExperimentText() { | |||||
| if (timerRef.current) { | if (timerRef.current) { | ||||
| clearTimeout(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); | item.imgName = item.img.slice(0, item.img.length - 4); | ||||
| }); | }); | ||||
| workflowRef.current = dag; | workflowRef.current = dag; | ||||
| getExperimentInstance(true); | |||||
| getExperimentInstance(); | |||||
| } catch (error) { | } catch (error) { | ||||
| // JSON.parse 错误 | // JSON.parse 错误 | ||||
| console.log(error); | console.log(error); | ||||
| @@ -77,48 +82,30 @@ function ExperimentText() { | |||||
| }; | }; | ||||
| // 获取实验实例 | // 获取实验实例 | ||||
| const getExperimentInstance = async (first) => { | |||||
| const getExperimentInstance = async () => { | |||||
| const [res] = await to(getExperimentIns(locationParams.id)); | const [res] = await to(getExperimentIns(locationParams.id)); | ||||
| if (res && res.data && workflowRef.current) { | if (res && res.data && workflowRef.current) { | ||||
| setExperimentIns(res.data); | 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 workflowData = workflowRef.current; | ||||
| const experimentStatusObjs = JSON.parse(nodes_status); | const experimentStatusObjs = JSON.parse(nodes_status); | ||||
| workflowData.nodes.forEach((item) => { | 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); | |||||
| if (first && status === ExperimentStatus.Pending) { | |||||
| // 如果状态是 Pending, 打开第一个节点 | |||||
| // 如果状态是 Running,打开第一个运行中的节点,如果没有运行中的节点,打开第一个节点 | |||||
| if (status === ExperimentStatus.Pending) { | |||||
| const node = workflowData.nodes[0]; | const node = workflowData.nodes[0]; | ||||
| if (node) { | if (node) { | ||||
| setExperimentNodeData(node); | setExperimentNodeData(node); | ||||
| openPropsDrawer(); | openPropsDrawer(); | ||||
| } | } | ||||
| } else if (first && status === ExperimentStatus.Running) { | |||||
| } else if (status === ExperimentStatus.Running) { | |||||
| const node = | const node = | ||||
| workflowData.nodes.find((item) => item.experimentStatus === ExperimentStatus.Running) ?? | workflowData.nodes.find((item) => item.experimentStatus === ExperimentStatus.Running) ?? | ||||
| workflowData.nodes[0]; | workflowData.nodes[0]; | ||||
| @@ -127,9 +114,83 @@ function ExperimentText() { | |||||
| openPropsDrawer(); | 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) => { | const getGraphData = (data, first) => { | ||||
| if (graph) { | if (graph) { | ||||
| @@ -149,7 +210,7 @@ function ExperimentText() { | |||||
| } | } | ||||
| } else { | } else { | ||||
| setTimeout(() => { | setTimeout(() => { | ||||
| getGraphData(data); | |||||
| getGraphData(data, first); | |||||
| }, 500); | }, 500); | ||||
| } | } | ||||
| }; | }; | ||||
| @@ -390,6 +451,10 @@ function ExperimentText() { | |||||
| graph.on('node:mouseleave', (e) => { | graph.on('node:mouseleave', (e) => { | ||||
| graph.setItemState(e.item, 'hover', false); | graph.setItemState(e.item, 'hover', false); | ||||
| }); | }); | ||||
| graph.on('canvas:click', (e) => { | |||||
| setExperimentNodeData(null); | |||||
| closePropsDrawer(); | |||||
| }); | |||||
| }; | }; | ||||
| return ( | return ( | ||||
| @@ -425,18 +490,19 @@ function ExperimentText() { | |||||
| </Button> | </Button> | ||||
| </div> | </div> | ||||
| <div className={styles['pipeline-container__graph']} ref={graphRef}></div> | <div className={styles['pipeline-container__graph']} ref={graphRef}></div> | ||||
| {experimentNodeData ? ( | |||||
| {experimentIns && experimentNodeData ? ( | |||||
| <ExperimentDrawer | <ExperimentDrawer | ||||
| key={experimentNodeData.id} | |||||
| open={propsDrawerOpen} | open={propsDrawerOpen} | ||||
| onClose={closePropsDrawer} | 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} | instanceNodeData={experimentNodeData} | ||||
| workflowId={experimentNodeData.workflowId} | workflowId={experimentNodeData.workflowId} | ||||
| instanceNodeStatus={experimentNodeData.experimentStatus} | instanceNodeStatus={experimentNodeData.experimentStatus} | ||||
| instanceNodeStartTime={experimentNodeData.experimentStartTime} | instanceNodeStartTime={experimentNodeData.experimentStartTime} | ||||
| instanceNodeEndTime={experimentIns.experimentEndTime} | |||||
| instanceNodeEndTime={experimentNodeData.experimentEndTime} | |||||
| ></ExperimentDrawer> | ></ExperimentDrawer> | ||||
| ) : null} | ) : null} | ||||
| <ParamsModal | <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'; | import styles from './index.less'; | ||||
| type ExperimentResultProps = { | type ExperimentResultProps = { | ||||
| experimentInsId?: number; // 实验实例 id | |||||
| pipelineNodeId?: string; // 流水线节点 id | |||||
| experimentInsId: number; // 实验实例 id | |||||
| pipelineNodeId: string; // 流水线节点 id | |||||
| }; | }; | ||||
| type ExperimentResultData = { | type ExperimentResultData = { | ||||
| @@ -6,12 +6,13 @@ | |||||
| import { ExperimentStatus } from '@/enums'; | import { ExperimentStatus } from '@/enums'; | ||||
| import { useStateRef } from '@/hooks'; | import { useStateRef } from '@/hooks'; | ||||
| import { ExperimentLog } from '@/pages/Experiment/Info/props'; | |||||
| import { getExperimentPodsLog } from '@/services/experiment/index.js'; | import { getExperimentPodsLog } from '@/services/experiment/index.js'; | ||||
| import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons'; | import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons'; | ||||
| import { Button } from 'antd'; | import { Button } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import dayjs from 'dayjs'; | |||||
| import { useEffect, useRef, useState } from 'react'; | import { useEffect, useRef, useState } from 'react'; | ||||
| import { ExperimentLog } from '../LogList'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| export type LogGroupProps = ExperimentLog & { | export type LogGroupProps = ExperimentLog & { | ||||
| @@ -21,6 +22,7 @@ export type LogGroupProps = ExperimentLog & { | |||||
| type Log = { | type Log = { | ||||
| start_time: string; // 日志开始时间 | start_time: string; // 日志开始时间 | ||||
| log_content: string; // 日志内容 | log_content: string; // 日志内容 | ||||
| pod_name: string; // pod名称 | |||||
| }; | }; | ||||
| // 滚动到底部 | // 滚动到底部 | ||||
| @@ -49,44 +51,33 @@ function LogGroup({ | |||||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
| const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | ||||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | ||||
| const socketRef = useRef<WebSocket | undefined>(undefined); | |||||
| const retryRef = useRef(2); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| scrollToBottom(false); | scrollToBottom(false); | ||||
| let timerId: NodeJS.Timeout | undefined; | |||||
| if (status === ExperimentStatus.Running) { | if (status === ExperimentStatus.Running) { | ||||
| timerId = setInterval(() => { | |||||
| requestExperimentPodsLog(); | |||||
| }, 5 * 1000); | |||||
| setupSockect(); | |||||
| } else if (preStatusRef.current === ExperimentStatus.Running) { | } else if (preStatusRef.current === ExperimentStatus.Running) { | ||||
| requestExperimentPodsLog(); | |||||
| setTimeout(() => { | |||||
| requestExperimentPodsLog(); | |||||
| }, 5 * 1000); | |||||
| setCompleted(true); | |||||
| } | } | ||||
| preStatusRef.current = status; | preStatusRef.current = status; | ||||
| return () => { | |||||
| if (timerId) { | |||||
| clearInterval(timerId); | |||||
| timerId = undefined; | |||||
| } | |||||
| }; | |||||
| }, [status]); | }, [status]); | ||||
| // 鼠标拖到中不滚动到底部 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const mouseDown = () => { | const mouseDown = () => { | ||||
| setIsMouseDown(true); | setIsMouseDown(true); | ||||
| }; | }; | ||||
| const mouseUp = () => { | const mouseUp = () => { | ||||
| setIsMouseDown(false); | setIsMouseDown(false); | ||||
| }; | }; | ||||
| document.addEventListener('mousedown', mouseDown); | document.addEventListener('mousedown', mouseDown); | ||||
| document.addEventListener('mouseup', mouseUp); | document.addEventListener('mouseup', mouseUp); | ||||
| return () => { | return () => { | ||||
| document.removeEventListener('mousedown', mouseDown); | document.removeEventListener('mousedown', mouseDown); | ||||
| document.removeEventListener('mouseup', mouseUp); | document.removeEventListener('mouseup', mouseUp); | ||||
| closeSocket(); | |||||
| }; | }; | ||||
| }, []); | }, []); | ||||
| @@ -140,6 +131,85 @@ function LogGroup({ | |||||
| requestExperimentPodsLog(); | 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 showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; | ||||
| const logText = log_content + logList.map((v) => v.log_content).join(''); | const logText = log_content + logList.map((v) => v.log_content).join(''); | ||||
| const showMoreBtn = | const showMoreBtn = | ||||
| @@ -1,16 +1,22 @@ | |||||
| import { ExperimentStatus } from '@/enums'; | import { ExperimentStatus } from '@/enums'; | ||||
| import { ExperimentLog } from '@/pages/Experiment/Info/props'; | |||||
| import { getQueryByExperimentLog } from '@/services/experiment/index.js'; | import { getQueryByExperimentLog } from '@/services/experiment/index.js'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import dayjs from 'dayjs'; | import dayjs from 'dayjs'; | ||||
| import { useEffect, useState } from 'react'; | |||||
| import { useEffect, useRef, useState } from 'react'; | |||||
| import LogGroup from '../LogGroup'; | import LogGroup from '../LogGroup'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| export type ExperimentLog = { | |||||
| log_type: 'normal' | 'resource'; // 日志类型 | |||||
| pod_name?: string; // 分布式名称 | |||||
| log_content?: string; // 日志内容 | |||||
| start_time?: string; // 日志开始时间 | |||||
| }; | |||||
| type LogListProps = { | type LogListProps = { | ||||
| instanceName?: string; // 实验实例 name | |||||
| instanceNamespace?: string; // 实验实例 namespace | |||||
| pipelineNodeId?: string; // 流水线节点 id | |||||
| instanceName: string; // 实验实例 name | |||||
| instanceNamespace: string; // 实验实例 namespace | |||||
| pipelineNodeId: string; // 流水线节点 id | |||||
| workflowId?: string; // 实验实例工作流 id | workflowId?: string; // 实验实例工作流 id | ||||
| instanceNodeStartTime?: string; // 实验实例节点开始运行时间 | instanceNodeStartTime?: string; // 实验实例节点开始运行时间 | ||||
| instanceNodeStatus?: ExperimentStatus; | instanceNodeStatus?: ExperimentStatus; | ||||
| @@ -25,23 +31,30 @@ function LogList({ | |||||
| instanceNodeStatus, | instanceNodeStatus, | ||||
| }: LogListProps) { | }: LogListProps) { | ||||
| const [logList, setLogList] = useState<ExperimentLog[]>([]); | const [logList, setLogList] = useState<ExperimentLog[]>([]); | ||||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | |||||
| const retryRef = useRef(3); | |||||
| useEffect(() => { | 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)); | const [res] = await to(getQueryByExperimentLog(params)); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| const { log_type, pods, log_detail } = res.data; | const { log_type, pods, log_detail } = res.data; | ||||
| @@ -62,6 +75,13 @@ function LogList({ | |||||
| })); | })); | ||||
| setLogList(list); | 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; | let graph = null; | ||||
| const EditPipeline = () => { | const EditPipeline = () => { | ||||
| const navgite = useNavigate(); | |||||
| const navigate = useNavigate(); | |||||
| const locationParams = useParams(); //新版本获取路由参数接口 | const locationParams = useParams(); //新版本获取路由参数接口 | ||||
| const graphRef = useRef(); | const graphRef = useRef(); | ||||
| const paramsDrawerRef = useRef(); | const paramsDrawerRef = useRef(); | ||||
| @@ -104,9 +104,7 @@ const EditPipeline = () => { | |||||
| setTimeout(() => { | setTimeout(() => { | ||||
| const data = graph.save(); | const data = graph.save(); | ||||
| console.log(data); | console.log(data); | ||||
| const errorNode = data.nodes.find((item) => { | |||||
| return item.formError === true; | |||||
| }); | |||||
| const errorNode = data.nodes.find((item) => item.formError === true); | |||||
| if (errorNode) { | if (errorNode) { | ||||
| message.error(`【${errorNode.label}】节点必填项必须配置`); | message.error(`【${errorNode.label}】节点必填项必须配置`); | ||||
| const graphNode = graph.findById(errorNode.id); | const graphNode = graph.findById(errorNode.id); | ||||
| @@ -124,7 +122,7 @@ const EditPipeline = () => { | |||||
| message.success('保存成功'); | message.success('保存成功'); | ||||
| setTimeout(() => { | setTimeout(() => { | ||||
| if (val) { | if (val) { | ||||
| navgite({ pathname: `/pipeline/template` }); | |||||
| navigate({ pathname: `/pipeline/template` }); | |||||
| } | } | ||||
| }, 500); | }, 500); | ||||
| }); | }); | ||||
| @@ -31,7 +31,7 @@ public interface ExperimentInsService { | |||||
| /** | /** | ||||
| * 根据实验ID查找所有具有相同ID的实例,并将它们添加到实验列表中 | * 根据实验ID查找所有具有相同ID的实例,并将它们添加到实验列表中 | ||||
| * | * | ||||
| * @param experimentId 实验id,不是实例id | |||||
| * @param experimentId 实验id,不是实验实例id | |||||
| * @return 实例列表 | * @return 实例列表 | ||||
| */ | */ | ||||
| List<ExperimentIns> getByExperimentId(Integer experimentId) throws IOException; | List<ExperimentIns> getByExperimentId(Integer experimentId) throws IOException; | ||||
| @@ -341,6 +341,9 @@ public class ImageServiceImpl implements ImageService { | |||||
| } | } | ||||
| } | } | ||||
| @Override | @Override | ||||
| public Map<String, String> uploadImageFiles(MultipartFile file) throws Exception { | public Map<String, String> uploadImageFiles(MultipartFile file) throws Exception { | ||||
| LoginUser loginUser = SecurityUtils.getLoginUser(); | LoginUser loginUser = SecurityUtils.getLoginUser(); | ||||
| @@ -99,10 +99,7 @@ public class JupyterServiceImpl implements JupyterService { | |||||
| // 调用修改后的 createPod 方法,传入额外的参数 | // 调用修改后的 createPod 方法,传入额外的参数 | ||||
| Integer podPort = k8sClientUtil.createConfiguredPod(podName, namespace, port, mountPath, pvc, image, minioPvcName, datasetPath, modelPath); | 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; | String url = masterIp + ":" + podPort; | ||||
| redisService.setCacheObject(podName,masterIp + ":" + podPort); | redisService.setCacheObject(podName,masterIp + ":" + podPort); | ||||
| devEnvironment.setStatus("Pending"); | devEnvironment.setStatus("Pending"); | ||||
| @@ -129,7 +126,6 @@ public class JupyterServiceImpl implements JupyterService { | |||||
| // 使用 Kubernetes API 删除 Pod | // 使用 Kubernetes API 删除 Pod | ||||
| String deleteResult = k8sClientUtil.deletePod(podName, namespace); | String deleteResult = k8sClientUtil.deletePod(podName, namespace); | ||||
| devEnvironment.setStatus("Terminated"); | devEnvironment.setStatus("Terminated"); | ||||
| this.devEnvironmentService.update(devEnvironment); | this.devEnvironmentService.update(devEnvironment); | ||||
| return deleteResult + ",编辑器已停止"; | return deleteResult + ",编辑器已停止"; | ||||
| @@ -19,58 +19,90 @@ public class AIM64EncoderUtil { | |||||
| } | } | ||||
| public static String aim64encode(Map<String, Object> value) { | public static String aim64encode(Map<String, Object> value) { | ||||
| // 将Map对象转换为JSON字符串 | |||||
| String jsonEncoded = JSON.toJSONString(value); | String jsonEncoded = JSON.toJSONString(value); | ||||
| // 将JSON字符串进行Base64编码 | |||||
| String base64Encoded = Base64.getEncoder().encodeToString(jsonEncoded.getBytes()); | String base64Encoded = Base64.getEncoder().encodeToString(jsonEncoded.getBytes()); | ||||
| String aim64Encoded = base64Encoded; | String aim64Encoded = base64Encoded; | ||||
| // 替换Base64编码中的特定字符 | |||||
| for (Map.Entry<String, String> entry : BS64_REPLACE_CHARACTERS_ENCODING.entrySet()) { | for (Map.Entry<String, String> entry : BS64_REPLACE_CHARACTERS_ENCODING.entrySet()) { | ||||
| aim64Encoded = aim64Encoded.replace(entry.getKey(), entry.getValue()); | aim64Encoded = aim64Encoded.replace(entry.getKey(), entry.getValue()); | ||||
| } | } | ||||
| // 返回带有前缀的AIM64编码字符串 | |||||
| return AIM64_ENCODING_PREFIX + aim64Encoded; | return AIM64_ENCODING_PREFIX + aim64Encoded; | ||||
| } | } | ||||
| // 这是一个静态方法,用于将 Map 对象 value 编码为字符串 | |||||
| public static String encode(Map<String, Object> value, boolean oneWayHashing) { | public static String encode(Map<String, Object> value, boolean oneWayHashing) { | ||||
| // 如果 oneWayHashing 参数为 true,表示使用一种不可逆的哈希算法 | |||||
| if (oneWayHashing) { | if (oneWayHashing) { | ||||
| // 使用 MD5 算法将 value 对象转换为字符串 | |||||
| return md5(JSON.toJSONString(value)); | return md5(JSON.toJSONString(value)); | ||||
| } | } | ||||
| // 如果 oneWayHashing 参数为 false,表示使用一种可逆的编码算法 | |||||
| return aim64encode(value); | return aim64encode(value); | ||||
| } | } | ||||
| // MD5 加密函数,用于将输入字符串转换为 MD5 加密后的字符串。 | |||||
| private static String md5(String input) { | private static String md5(String input) { | ||||
| // 尝试获取 MD5 加密对象 | |||||
| try { | try { | ||||
| java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); | java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); | ||||
| // 使用 MD5 对象对输入字符串进行加密 | |||||
| byte[] array = md.digest(input.getBytes()); | byte[] array = md.digest(input.getBytes()); | ||||
| // 创建一个 StringBuilder 对象,用于将加密后的字节转换为字符串 | |||||
| StringBuilder sb = new StringBuilder(); | StringBuilder sb = new StringBuilder(); | ||||
| // 遍历加密后的字节数组 | |||||
| for (byte b : array) { | for (byte b : array) { | ||||
| // 将每个字节转换为 16 进制字符串 | |||||
| sb.append(Integer.toHexString((b & 0xFF) | 0x100).substring(1, 3)); | sb.append(Integer.toHexString((b & 0xFF) | 0x100).substring(1, 3)); | ||||
| } | } | ||||
| // 返回加密后的字符串 | |||||
| return sb.toString(); | return sb.toString(); | ||||
| } catch (java.security.NoSuchAlgorithmException e) { | } catch (java.security.NoSuchAlgorithmException e) { | ||||
| // 如果在获取 MD5 加密对象时出现了错误,则打印错误信息 | |||||
| e.printStackTrace(); | e.printStackTrace(); | ||||
| } | } | ||||
| // 如果出现了错误,则返回 null | |||||
| return null; | return null; | ||||
| } | } | ||||
| // 这是一个 decode 方法,用于将 runIds 列表转换为查询字符串 | |||||
| public static String decode(List<String> runIds) { | public static String decode(List<String> runIds) { | ||||
| // 确保 runIds 列表的大小为 3 | // 确保 runIds 列表的大小为 3 | ||||
| if (runIds == null || runIds.size() == 0) { | if (runIds == null || runIds.size() == 0) { | ||||
| // 如果 runIds 为空,抛出 IllegalArgumentException 异常 | |||||
| throw new IllegalArgumentException("runIds 不能为空"); | throw new IllegalArgumentException("runIds 不能为空"); | ||||
| } | } | ||||
| // 构建查询字符串 | // 构建查询字符串 | ||||
| StringBuilder queryBuilder = new StringBuilder("run.hash in ["); | StringBuilder queryBuilder = new StringBuilder("run.hash in ["); | ||||
| // 遍历 runIds 列表 | |||||
| for (int i = 0; i < runIds.size(); i++) { | for (int i = 0; i < runIds.size(); i++) { | ||||
| // 如果当前不是第一个元素,添加逗号 | |||||
| if (i > 0) { | if (i > 0) { | ||||
| queryBuilder.append(","); | queryBuilder.append(","); | ||||
| } | } | ||||
| // 将当前元素添加到查询字符串中,使用双引号括起来 | |||||
| queryBuilder.append("\"").append(runIds.get(i)).append("\""); | queryBuilder.append("\"").append(runIds.get(i)).append("\""); | ||||
| } | } | ||||
| // 将查询字符串闭合 | |||||
| queryBuilder.append("]"); | queryBuilder.append("]"); | ||||
| // 将查询字符串转换为字符串 | |||||
| String query = queryBuilder.toString(); | String query = queryBuilder.toString(); | ||||
| // 创建一个 Map 对象,用于存储查询参数 | |||||
| Map<String, Object> map = new HashMap<>(); | Map<String, Object> map = new HashMap<>(); | ||||
| // 将查询字符串添加到 Map 中 | |||||
| map.put("query", query); | map.put("query", query); | ||||
| // 将 advancedMode 设置为 true | |||||
| map.put("advancedMode", true); | map.put("advancedMode", true); | ||||
| // 将 advancedQuery 设置为查询字符串 | |||||
| map.put("advancedQuery", query); | map.put("advancedQuery", query); | ||||
| // 使用 encode 方法将 Map 对象转换为查询字符串 | |||||
| String searchQuery = encode(map, false); | String searchQuery = encode(map, false); | ||||
| // 返回查询字符串 | |||||
| return searchQuery; | return searchQuery; | ||||
| } | } | ||||
| } | } | ||||
| @@ -9,19 +9,30 @@ import java.util.Set; | |||||
| public class BeansUtils { | public class BeansUtils { | ||||
| // 获取一个 Java 对象中所有为空的属性名称。: | |||||
| public static String[] getNullPropertyNames(Object source) { | public static String[] getNullPropertyNames(Object source) { | ||||
| // 创建一个 BeanWrapper 对象,参数为给定的 Java 对象 | |||||
| final BeanWrapper src = new BeanWrapperImpl(source); | final BeanWrapper src = new BeanWrapperImpl(source); | ||||
| // 获取该对象所有属性描述符 | |||||
| java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors(); | java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors(); | ||||
| // 创建一个集合,用于存储为空的属性名称 | |||||
| Set<String> emptyNames = new HashSet<String>(); | Set<String> emptyNames = new HashSet<String>(); | ||||
| // 遍历所有属性描述符 | |||||
| for(java.beans.PropertyDescriptor pd : pds) { | for(java.beans.PropertyDescriptor pd : pds) { | ||||
| // 获取属性值 | |||||
| Object srcValue = src.getPropertyValue(pd.getName()); | Object srcValue = src.getPropertyValue(pd.getName()); | ||||
| // 如果属性值为空,则将属性名称添加到集合中 | |||||
| if (srcValue == null) emptyNames.add(pd.getName()); | if (srcValue == null) emptyNames.add(pd.getName()); | ||||
| } | } | ||||
| // 创建一个字符串数组,用于存储为空的属性名称 | |||||
| String[] result = new String[emptyNames.size()]; | String[] result = new String[emptyNames.size()]; | ||||
| // 将为空的属性名称存储到字符串数组中 | |||||
| return emptyNames.toArray(result); | return emptyNames.toArray(result); | ||||
| } | } | ||||
| public static void copyPropertiesIgnoreNull(Object src, Object target){ | public static void copyPropertiesIgnoreNull(Object src, Object target){ | ||||
| // 使用 BeanUtils 工具类将源对象中的非空属性复制到目标对象中 | |||||
| BeanUtils.copyProperties(src, target, getNullPropertyNames(src)); | BeanUtils.copyProperties(src, target, getNullPropertyNames(src)); | ||||
| } | } | ||||
| } | } | ||||
| @@ -19,6 +19,13 @@ import java.util.List; | |||||
| public class ConvertUtil { | public class ConvertUtil { | ||||
| public static final Logger logger = LoggerFactory.getLogger(ConvertUtil.class); | 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) { | public static <T> T entityToVo(Object source, Class<T> target) { | ||||
| if (source == null) { | if (source == null) { | ||||
| return null; | return null; | ||||
| @@ -33,6 +40,8 @@ public class ConvertUtil { | |||||
| return targetObject; | return targetObject; | ||||
| } | } | ||||
| // 将实体对象列表转换为目标类型的列表 | |||||
| public static <T> List<T> entityToVoList(Collection<?> sourceList, Class<T> target) { | public static <T> List<T> entityToVoList(Collection<?> sourceList, Class<T> target) { | ||||
| if (sourceList == null) { | if (sourceList == null) { | ||||
| return null; | return null; | ||||
| @@ -50,5 +59,6 @@ public class ConvertUtil { | |||||
| } | } | ||||
| return targetList; | return targetList; | ||||
| } | } | ||||
| } | } | ||||
| @@ -2,20 +2,29 @@ package com.ruoyi.platform.utils; | |||||
| public class FileUtil { | public class FileUtil { | ||||
| // 格式化文件大小为可读的字符串表示 | |||||
| public static String formatFileSize(long sizeInBytes) { | public static String formatFileSize(long sizeInBytes) { | ||||
| // 检查文件大小是否为负数 | |||||
| if (sizeInBytes < 0) { | if (sizeInBytes < 0) { | ||||
| throw new IllegalArgumentException("File size cannot be negative."); | throw new IllegalArgumentException("File size cannot be negative."); | ||||
| } | } | ||||
| // 如果文件大小小于1KB,直接返回字节数 | |||||
| if (sizeInBytes < 1024) { | if (sizeInBytes < 1024) { | ||||
| return sizeInBytes + " B"; | return sizeInBytes + " B"; | ||||
| } else if (sizeInBytes < 1024 * 1024) { | |||||
| } | |||||
| // 如果文件大小小于1MB,转换为KB并返回 | |||||
| else if (sizeInBytes < 1024 * 1024) { | |||||
| double sizeInKB = sizeInBytes / 1024.0; | double sizeInKB = sizeInBytes / 1024.0; | ||||
| return String.format("%.2f KB", sizeInKB); | 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); | double sizeInMB = sizeInBytes / (1024.0 * 1024); | ||||
| return String.format("%.2f MB", sizeInMB); | return String.format("%.2f MB", sizeInMB); | ||||
| } else { | |||||
| } | |||||
| // 如果文件大小大于或等于1GB,转换为GB并返回 | |||||
| else { | |||||
| double sizeInGB = sizeInBytes / (1024.0 * 1024 * 1024); | double sizeInGB = sizeInBytes / (1024.0 * 1024 * 1024); | ||||
| return String.format("%.2f GB", sizeInGB); | return String.format("%.2f GB", sizeInGB); | ||||
| } | } | ||||
| @@ -37,10 +37,8 @@ public class JsonUtils { | |||||
| // 将JSON字符串转换为扁平化的Map | // 将JSON字符串转换为扁平化的Map | ||||
| public static Map<String, Object> flattenJson(String prefix, Map<String, Object> map) { | public static Map<String, Object> flattenJson(String prefix, Map<String, Object> map) { | ||||
| Map<String, Object> flatMap = new HashMap<>(); | 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(); | String key = entry.getKey(); | ||||
| Object value = entry.getValue(); | Object value = entry.getValue(); | ||||
| @@ -371,7 +371,7 @@ public class K8sClientUtil { | |||||
| return service.getSpec().getPorts().get(0).getNodePort(); | 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) { | 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<>(); | Map<String, String> selector = new LinkedHashMap<>(); | ||||
| selector.put("k8s-jupyter", podName); | selector.put("k8s-jupyter", podName); | ||||