diff --git a/react-ui/src/assets/img/experiment-pending.png b/react-ui/src/assets/img/experiment-pending.png new file mode 100644 index 00000000..ceefa027 Binary files /dev/null and b/react-ui/src/assets/img/experiment-pending.png differ diff --git a/react-ui/src/assets/img/experiment-running.png b/react-ui/src/assets/img/experiment-running.png index 3155302a..d4121030 100644 Binary files a/react-ui/src/assets/img/experiment-running.png and b/react-ui/src/assets/img/experiment-running.png differ diff --git a/react-ui/src/hooks/index.ts b/react-ui/src/hooks/index.ts index b34d5156..adf61e7d 100644 --- a/react-ui/src/hooks/index.ts +++ b/react-ui/src/hooks/index.ts @@ -32,6 +32,7 @@ export function useStateRef(initialValue: T) { */ export function useVisible(initialValue: boolean) { const [visible, setVisible] = useState(initialValue); + const ref = useRef(initialValue); const open = useCallback(() => { setVisible(true); @@ -41,7 +42,11 @@ export function useVisible(initialValue: boolean) { setVisible(false); }, []); - return [visible, open, close] as const; + useEffect(() => { + ref.current = visible; + }, [visible]); + + return [visible, open, close, ref] as const; } type Callback = (state: T) => void; diff --git a/react-ui/src/pages/Experiment/Info/index.jsx b/react-ui/src/pages/Experiment/Info/index.jsx index f8325562..3c1eda2f 100644 --- a/react-ui/src/pages/Experiment/Info/index.jsx +++ b/react-ui/src/pages/Experiment/Info/index.jsx @@ -1,71 +1,39 @@ +import { ExperimentStatus } from '@/enums'; import { useStateRef, useVisible } from '@/hooks'; import { getExperimentIns } from '@/services/experiment/index.js'; import { getWorkflowById } from '@/services/pipeline/index.js'; import themes from '@/styles/theme.less'; import { fittingString } from '@/utils'; import { elapsedTime, formatDate } from '@/utils/date'; -import G6 from '@antv/g6'; +import { to } from '@/utils/promise'; +import G6, { Util } from '@antv/g6'; import { Button } from 'antd'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import ParamsModal from '../components/ViewParamsModal'; import { experimentStatusInfo } from '../status'; import styles from './index.less'; -import Props from './props'; +import ExperimentDrawer from './props'; let graph = null; function ExperimentText() { - const [message, setMessage, messageRef] = useStateRef({}); - const propsRef = useRef(); - const navgite = useNavigate(); + const [experimentIns, setExperimentIns] = useState(undefined); + const [experimentNodeData, setExperimentNodeData, experimentNodeDataRef] = useStateRef(undefined); + const graphRef = useRef(); + const timerRef = useRef(); + const workflowRef = useRef(); const locationParams = useParams(); // 新版本获取路由参数接口 const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); - const graphRef = useRef(); - - const getGraphData = (data) => { - if (graph) { - graph.data(data); - graph.render(); - } else { - setTimeout(() => { - getGraphData(data); - }, 500); - } - }; - const getFirstWorkflow = (val) => { - getWorkflowById(val).then((pipelineRes) => { - if (graph && pipelineRes.data && pipelineRes.data.dag) { - getExperimentIns(locationParams.id).then((experimentRes) => { - if (experimentRes.code === 200) { - setMessage(experimentRes.data); - const experimentStatusObjs = JSON.parse(experimentRes.data.nodes_status); - const newNodeList = JSON.parse(pipelineRes.data.dag).nodes.map((item) => { - return { - ...item, - experimentEndTime: experimentStatusObjs?.[item.id]?.finishedAt, - experimentStartTime: experimentStatusObjs?.[item.id]?.startedAt, - experimentStatus: experimentStatusObjs?.[item.id]?.phase, - component_id: experimentStatusObjs?.[item.id]?.id, - img: experimentStatusObjs?.[item.id]?.phase - ? item.img.slice(0, item.img.length - 4) + - '-' + - experimentStatusObjs[item.id].phase + - '.png' - : item.img, - }; - }); - const newData = { ...JSON.parse(pipelineRes.data.dag), nodes: newNodeList }; - getGraphData(newData); - } - }); - } - }); - }; + const [propsDrawerOpen, openPropsDrawer, closePropsDrawer, propsDrawerOpenRef] = + useVisible(false); + const navigate = useNavigate(); + const width = 110; + const height = 36; useEffect(() => { initGraph(); - getFirstWorkflow(locationParams.workflowId); + getWorkflow(); const changeSize = () => { if (!graph || graph.get('destroyed')) return; @@ -77,9 +45,115 @@ function ExperimentText() { window.addEventListener('resize', changeSize); return () => { window.removeEventListener('resize', changeSize); + if (timerRef.current) { + clearTimeout(timerRef.current); + } }; }, []); + useEffect(() => { + propsDrawerOpenRef.current = propsDrawerOpen; + }, [propsDrawerOpen]); + + // 获取流水线模版 + const getWorkflow = async () => { + const [res] = await to(getWorkflowById(locationParams.workflowId)); + if (res && res.data && res.data.dag) { + try { + const dag = JSON.parse(res.data.dag); + dag.nodes.forEach((item) => { + item.in_parameters = JSON.parse(item.in_parameters); + item.out_parameters = JSON.parse(item.out_parameters); + item.control_strategy = JSON.parse(item.control_strategy); + item.imgName = item.img.slice(0, item.img.length - 4); + }); + workflowRef.current = dag; + getExperimentInstance(true); + } catch (error) { + // JSON.parse 错误 + console.log(error); + } + } + }; + + // 获取实验实例 + const getExperimentInstance = async (first) => { + const [res] = await to(getExperimentIns(locationParams.id)); + if (res && res.data && workflowRef.current) { + setExperimentIns(res.data); + const { status, nodes_status } = 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`; + }); + + // 更新打开的抽屉数据 + 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); + } + + if (first && status === ExperimentStatus.Pending) { + const node = workflowData.nodes[0]; + if (node) { + setExperimentNodeData(node); + openPropsDrawer(); + } + } else if (first && status === ExperimentStatus.Running) { + const node = + workflowData.nodes.find((item) => item.experimentStatus === ExperimentStatus.Running) ?? + workflowData.nodes[0]; + if (node) { + setExperimentNodeData(node); + openPropsDrawer(); + } + } + } + }; + + // 根据数据,渲染图 + const getGraphData = (data, first) => { + if (graph) { + const zoom = graph.getZoom(); + // 在拉取新数据重新渲染页面之前先获取点(0, 0)在画布上的位置 + const lastPoint = graph.getCanvasByPoint(0, 0); + graph.data(data); + graph.render(); + if (first) { + graph.fitView(); + } else { + graph.zoomTo(zoom); + // 获取重新渲染之后点(0, 0)在画布的位置 + const newPoint = graph.getCanvasByPoint(0, 0); + // 移动画布相对位移; + graph.translate(lastPoint.x - newPoint.x, lastPoint.y - newPoint.y); + } + } else { + setTimeout(() => { + getGraphData(data); + }, 500); + } + }; + const initGraph = () => { G6.registerNode( 'rect-node', @@ -124,6 +198,54 @@ function ExperimentText() { draggable: true, }); } + const hasRightImg = + cfg.experimentStatus === ExperimentStatus.Pending || + cfg.experimentStatus === ExperimentStatus.Running; + if (hasRightImg) { + const image = group.addShape('image', { + attrs: { + x: -10, + y: -10, + width: 20, + height: 20, + img: + cfg.experimentStatus === ExperimentStatus.Pending + ? require('@/assets/img/experiment-pending.png') + : require('@/assets/img/experiment-running.png'), + cursor: 'pointer', + }, + draggable: false, + capture: false, + }); + + if (cfg.experimentStatus === ExperimentStatus.Running) { + image.animate( + (ratio) => { + const toMatrix = Util.transform( + [1, 0, 0, 0, 1, 0, 0, 0, 1], + [ + ['r', ratio * Math.PI * 2], + ['t', width / 2 - 14 + 10, -height / 2 - 6 + 10], + ], + ); + return { + matrix: toMatrix, + }; + }, + { + repeat: true, // 动画重复 + duration: 1000, + easing: 'easeLinear', + }, + ); + } else if (cfg.experimentStatus === ExperimentStatus.Pending) { + const toMatrix = Util.transform( + [1, 0, 0, 0, 1, 0, 0, 0, 1], + [['t', width / 2 - 14 + 10, -height / 2 - 6 + 10]], + ); + image.setMatrix(toMatrix); + } + } const bbox = group.getBBox(); const anchorPoints = this.getAnchorPoints(cfg); anchorPoints.forEach((anchorPos, i) => { @@ -147,12 +269,12 @@ function ExperimentText() { // response the state changes and show/hide the link-point circles setState(name, value, item) { const group = item.getContainer(); - const shape = group.get('children')[0]; + const shape = group.get('children')?.[0]; if (name === 'hover') { if (value) { - shape.attr('stroke', themes['primaryColor']); + shape?.attr('stroke', themes['primaryColor']); } else { - shape.attr('stroke', 'transparent'); + shape?.attr('stroke', 'transparent'); } } }, @@ -189,7 +311,7 @@ function ExperimentText() { defaultNode: { type: 'rect-node', - size: [110, 36], + size: [width, height], labelCfg: { style: { @@ -257,7 +379,9 @@ function ExperimentText() { const bindEvents = () => { graph.on('node:click', (e) => { if (e.target.get('name') !== 'anchor-point' && e.item) { - propsRef.current.showDrawer(e, locationParams.id, messageRef.current); + const model = e.item.getModel(); + setExperimentNodeData(model); + openPropsDrawer(); } }); graph.on('node:mouseenter', (e) => { @@ -272,11 +396,11 @@ function ExperimentText() {
- 启动时间:{formatDate(message.create_time)} + 启动时间:{formatDate(experimentIns?.create_time)}
执行时长: - {elapsedTime(message.create_time, message.finish_time)} + {elapsedTime(experimentIns?.create_time, experimentIns?.finish_time)}
状态: @@ -286,11 +410,11 @@ function ExperimentText() { height: '8px', borderRadius: '50%', marginRight: '6px', - backgroundColor: experimentStatusInfo[message.status]?.color, + backgroundColor: experimentStatusInfo[experimentIns?.status]?.color, }} >
- - {experimentStatusInfo[message.status]?.label} + + {experimentStatusInfo[experimentIns?.status]?.label}
- + {experimentNodeData ? ( + + ) : null} ); diff --git a/react-ui/src/pages/Experiment/Info/index.less b/react-ui/src/pages/Experiment/Info/index.less index f2f5510d..9004ce33 100644 --- a/react-ui/src/pages/Experiment/Info/index.less +++ b/react-ui/src/pages/Experiment/Info/index.less @@ -30,4 +30,10 @@ background-image: url(/assets/images/pipeline-canvas-back.png); background-size: 100% 100%; } + + :global { + .ant-drawer-mask { + background: transparent !important; + } + } } diff --git a/react-ui/src/pages/Experiment/Info/props.less b/react-ui/src/pages/Experiment/Info/props.less index b3294d55..1d5bdc34 100644 --- a/react-ui/src/pages/Experiment/Info/props.less +++ b/react-ui/src/pages/Experiment/Info/props.less @@ -14,7 +14,12 @@ border: 1px solid #e0eaff; } .ant-tabs-content-holder { - overflow-y: auto; + .ant-tabs-content { + height: 100%; + .ant-tabs-tabpane { + height: 100%; + } + } } } } diff --git a/react-ui/src/pages/Experiment/Info/props.tsx b/react-ui/src/pages/Experiment/Info/props.tsx index 48d72c0d..5940756a 100644 --- a/react-ui/src/pages/Experiment/Info/props.tsx +++ b/react-ui/src/pages/Experiment/Info/props.tsx @@ -1,11 +1,9 @@ -import { getNodeResult, getQueryByExperimentLog } from '@/services/experiment/index.js'; +import { ExperimentStatus } from '@/enums'; import { PipelineNodeModelSerialize } from '@/types'; import { elapsedTime, formatDate } from '@/utils/date'; -import { to } from '@/utils/promise'; import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; -import { Drawer, Form, Tabs } from 'antd'; -import dayjs from 'dayjs'; -import { forwardRef, useImperativeHandle, useState } from 'react'; +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'; @@ -19,154 +17,130 @@ export type ExperimentLog = { start_time?: string; // 日志开始时间 }; -const Props = forwardRef((_, ref) => { - const [form] = Form.useForm(); - const [experimentNodeData, setExperimentNodeData] = useState( - {} as PipelineNodeModelSerialize, - ); - const [experimentResults, setExperimentResults] = useState([]); - const [experimentLogList, setExperimentLogList] = useState([]); +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 items = [ - { - key: '1', - label: '日志详情', - children: ( - - ), - icon: , - }, - { - key: '2', - label: '配置参数', - icon: , - children: , - }, +const ExperimentDrawer = forwardRef( + ( { - key: '3', - label: '输出结果', - children: , - icon: , - }, - ]; - const [open, setOpen] = useState(false); - const onClose = () => { - setOpen(false); - }; - - // 获取实验日志 - const getExperimentLog = async (params: any, start_time: number) => { - const [res] = await to(getQueryByExperimentLog(params)); - if (res && res.data) { - const { log_type, pods, log_detail } = res.data; - if (log_type === 'normal') { - const list = [ - { - ...log_detail, - log_type, - }, - ]; - setExperimentLogList(list); - } else if (log_type === 'resource') { - const list = pods.map((v: string) => ({ - log_type, - pod_name: v, - log_content: '', - start_time, - })); - setExperimentLogList(list); - } - } - }; - - // 获取实验结果 - const getExperimentResult = async (params: any) => { - const [res] = await to(getNodeResult(params)); - if (res && res.data) { - setExperimentResults(res.data); - } - }; + open, + onClose, + instanceId, + instanceName, + instanceNamespace, + instanceNodeData, + workflowId, + instanceNodeStatus, + instanceNodeStartTime, + instanceNodeEndTime, + }: ExperimentDrawerProps, + ref, + ) => { + useImperativeHandle(ref, () => ({})); - useImperativeHandle(ref, () => ({ - showDrawer(e: any, id: string, message: any) { - setOpen(true); + // 如果性能有问题,可以进一步拆解 + 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, + ], + ); - // 获取实验参数 - const model = e.item.getModel(); - try { - const nodeData = { - ...model, - in_parameters: JSON.parse(model.in_parameters), - out_parameters: JSON.parse(model.out_parameters), - control_strategy: JSON.parse(model.control_strategy), - }; - setExperimentNodeData(nodeData); - form.setFieldsValue(nodeData); - } catch (error) { - console.log(error); - } - - // 获取实验日志和实验结果 - setExperimentLogList([]); - setExperimentResults([]); - // 如果已经运行到了 - if (e.item?.getModel()?.component_id) { - const model = e.item.getModel(); - const start_time = dayjs(model.experimentStartTime).valueOf() * 1.0e6; - const params = { - task_id: model.id, - component_id: model.component_id, - name: message.argo_ins_name, - namespace: message.argo_ins_ns, - start_time: start_time, - }; - getExperimentLog(params, start_time); - getExperimentResult({ id, node_id: model.id }); - } - }, - })); - return ( - -
-
- 任务名称:{experimentNodeData.label} -
-
- 执行状态: -
- - {experimentStatusInfo[experimentNodeData.experimentStatus]?.label} - -
-
- 启动时间:{formatDate(experimentNodeData.experimentStartTime)} -
-
- 耗时: - {elapsedTime( - experimentNodeData.experimentStartTime, - experimentNodeData.experimentEndTime, - )} + return ( + +
+
+ 任务名称:{instanceNodeData.label} +
+
+ 执行状态: + {instanceNodeStatus ? ( + <> +
+ + {experimentStatusInfo[instanceNodeStatus]?.label} + + + ) : ( + '--' + )} +
+
+ 启动时间:{formatDate(instanceNodeStartTime)} +
+
+ 耗时: + {elapsedTime(instanceNodeStartTime, instanceNodeEndTime)} +
-
- - - ); -}); + + + ); + }, +); -export default Props; +export default ExperimentDrawer; diff --git a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.less b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.less index 44b590ea..c5d9824e 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.less +++ b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.less @@ -1,5 +1,7 @@ .experiment-parameter { + height: 100%; padding-top: 8px; + overflow-y: auto; &__title { display: flex; diff --git a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx index 62be954f..eb516935 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx @@ -3,16 +3,15 @@ import ParameterSelect from '@/components/ParameterSelect'; import SubAreaTitle from '@/components/SubAreaTitle'; import { useComputingResource } from '@/hooks/resource'; import { PipelineNodeModelSerialize } from '@/types'; -import { Form, Input, Select, type FormProps } from 'antd'; +import { Form, Input, Select } from 'antd'; import styles from './index.less'; const { TextArea } = Input; type ExperimentParameterProps = { - form: FormProps['form']; nodeData: PipelineNodeModelSerialize; }; -function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) { +function ExperimentParameter({ nodeData }: ExperimentParameterProps) { const [resourceStandardList] = useComputingResource(); // 资源规模 // 控制策略 @@ -42,7 +41,7 @@ function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) { wrapperCol={{ span: 24, }} - form={form} + initialValues={nodeData} style={{ maxWidth: 600, }} diff --git a/react-ui/src/pages/Experiment/components/ExperimentResult/index.less b/react-ui/src/pages/Experiment/components/ExperimentResult/index.less index 078fe4f2..78684d72 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentResult/index.less +++ b/react-ui/src/pages/Experiment/components/ExperimentResult/index.less @@ -1,5 +1,7 @@ .experiment-result { + height: 100%; padding: 8px; + overflow-y: auto; color: @text-color; font-size: 14px; diff --git a/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx index e3cbd4da..feabcc3d 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx @@ -1,11 +1,15 @@ +import { getNodeResult } from '@/services/experiment/index.js'; import { downLoadZip } from '@/utils/downloadfile'; import { openAntdModal } from '@/utils/modal'; +import { to } from '@/utils/promise'; import { App, Button } from 'antd'; +import { useEffect, useState } from 'react'; import ExportModelModal from '../ExportModelModal'; import styles from './index.less'; type ExperimentResultProps = { - results?: ExperimentResultData[] | null; + experimentInsId?: number; // 实验实例 id + pipelineNodeId?: string; // 流水线节点 id }; type ExperimentResultData = { @@ -18,8 +22,21 @@ type ExperimentResultData = { }[]; }; -function ExperimentResult({ results }: ExperimentResultProps) { +function ExperimentResult({ experimentInsId, pipelineNodeId }: ExperimentResultProps) { const { message } = App.useApp(); + const [experimentResults, setExperimentResults] = useState([]); + + useEffect(() => { + getExperimentResult({ id: `${experimentInsId}`, node_id: pipelineNodeId }); + }, []); + + // 获取实验结果 + const getExperimentResult = async (params: any) => { + const [res] = await to(getNodeResult(params)); + if (res && res.data) { + setExperimentResults(res.data); + } + }; // 下载 const download = (path: string) => { @@ -40,9 +57,9 @@ function ExperimentResult({ results }: ExperimentResultProps) { return (
- {results && results.length > 0 ? ( - results.map((item) => ( -
+ {experimentResults.length > 0 ? ( + experimentResults.map((item) => ( +
{item.name}