| @@ -32,6 +32,7 @@ export function useStateRef<T>(initialValue: T) { | |||||
| */ | */ | ||||
| export function useVisible(initialValue: boolean) { | export function useVisible(initialValue: boolean) { | ||||
| const [visible, setVisible] = useState(initialValue); | const [visible, setVisible] = useState(initialValue); | ||||
| const ref = useRef(initialValue); | |||||
| const open = useCallback(() => { | const open = useCallback(() => { | ||||
| setVisible(true); | setVisible(true); | ||||
| @@ -41,7 +42,11 @@ export function useVisible(initialValue: boolean) { | |||||
| setVisible(false); | setVisible(false); | ||||
| }, []); | }, []); | ||||
| return [visible, open, close] as const; | |||||
| useEffect(() => { | |||||
| ref.current = visible; | |||||
| }, [visible]); | |||||
| return [visible, open, close, ref] as const; | |||||
| } | } | ||||
| type Callback<T> = (state: T) => void; | type Callback<T> = (state: T) => void; | ||||
| @@ -1,71 +1,39 @@ | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { useStateRef, useVisible } from '@/hooks'; | import { useStateRef, useVisible } from '@/hooks'; | ||||
| import { getExperimentIns } from '@/services/experiment/index.js'; | import { getExperimentIns } from '@/services/experiment/index.js'; | ||||
| import { getWorkflowById } from '@/services/pipeline/index.js'; | import { getWorkflowById } from '@/services/pipeline/index.js'; | ||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { fittingString } from '@/utils'; | import { fittingString } from '@/utils'; | ||||
| import { elapsedTime, formatDate } from '@/utils/date'; | 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 { Button } from 'antd'; | ||||
| import { useEffect, useRef } from 'react'; | |||||
| import { useEffect, useRef, useState } from 'react'; | |||||
| import { useNavigate, useParams } from 'react-router-dom'; | import { useNavigate, useParams } from 'react-router-dom'; | ||||
| 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 Props from './props'; | |||||
| import ExperimentDrawer from './props'; | |||||
| let graph = null; | let graph = null; | ||||
| function ExperimentText() { | 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 locationParams = useParams(); // 新版本获取路由参数接口 | ||||
| const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); | 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(() => { | useEffect(() => { | ||||
| initGraph(); | initGraph(); | ||||
| getFirstWorkflow(locationParams.workflowId); | |||||
| getWorkflow(); | |||||
| const changeSize = () => { | const changeSize = () => { | ||||
| if (!graph || graph.get('destroyed')) return; | if (!graph || graph.get('destroyed')) return; | ||||
| @@ -77,9 +45,115 @@ function ExperimentText() { | |||||
| window.addEventListener('resize', changeSize); | window.addEventListener('resize', changeSize); | ||||
| return () => { | return () => { | ||||
| window.removeEventListener('resize', changeSize); | 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 = () => { | const initGraph = () => { | ||||
| G6.registerNode( | G6.registerNode( | ||||
| 'rect-node', | 'rect-node', | ||||
| @@ -124,6 +198,54 @@ function ExperimentText() { | |||||
| draggable: true, | 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 bbox = group.getBBox(); | ||||
| const anchorPoints = this.getAnchorPoints(cfg); | const anchorPoints = this.getAnchorPoints(cfg); | ||||
| anchorPoints.forEach((anchorPos, i) => { | anchorPoints.forEach((anchorPos, i) => { | ||||
| @@ -147,12 +269,12 @@ function ExperimentText() { | |||||
| // response the state changes and show/hide the link-point circles | // response the state changes and show/hide the link-point circles | ||||
| setState(name, value, item) { | setState(name, value, item) { | ||||
| const group = item.getContainer(); | const group = item.getContainer(); | ||||
| const shape = group.get('children')[0]; | |||||
| const shape = group.get('children')?.[0]; | |||||
| if (name === 'hover') { | if (name === 'hover') { | ||||
| if (value) { | if (value) { | ||||
| shape.attr('stroke', themes['primaryColor']); | |||||
| shape?.attr('stroke', themes['primaryColor']); | |||||
| } else { | } else { | ||||
| shape.attr('stroke', 'transparent'); | |||||
| shape?.attr('stroke', 'transparent'); | |||||
| } | } | ||||
| } | } | ||||
| }, | }, | ||||
| @@ -189,7 +311,7 @@ function ExperimentText() { | |||||
| defaultNode: { | defaultNode: { | ||||
| type: 'rect-node', | type: 'rect-node', | ||||
| size: [110, 36], | |||||
| size: [width, height], | |||||
| labelCfg: { | labelCfg: { | ||||
| style: { | style: { | ||||
| @@ -257,7 +379,9 @@ function ExperimentText() { | |||||
| const bindEvents = () => { | const bindEvents = () => { | ||||
| graph.on('node:click', (e) => { | graph.on('node:click', (e) => { | ||||
| if (e.target.get('name') !== 'anchor-point' && e.item) { | 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) => { | graph.on('node:mouseenter', (e) => { | ||||
| @@ -272,11 +396,11 @@ function ExperimentText() { | |||||
| <div className={styles['pipeline-container']}> | <div className={styles['pipeline-container']}> | ||||
| <div className={styles['pipeline-container__top']}> | <div className={styles['pipeline-container__top']}> | ||||
| <div className={styles['pipeline-container__top__info']}> | <div className={styles['pipeline-container__top__info']}> | ||||
| 启动时间:{formatDate(message.create_time)} | |||||
| 启动时间:{formatDate(experimentIns?.create_time)} | |||||
| </div> | </div> | ||||
| <div className={styles['pipeline-container__top__info']}> | <div className={styles['pipeline-container__top__info']}> | ||||
| 执行时长: | 执行时长: | ||||
| {elapsedTime(message.create_time, message.finish_time)} | |||||
| {elapsedTime(experimentIns?.create_time, experimentIns?.finish_time)} | |||||
| </div> | </div> | ||||
| <div className={styles['pipeline-container__top__info']}> | <div className={styles['pipeline-container__top__info']}> | ||||
| 状态: | 状态: | ||||
| @@ -286,11 +410,11 @@ function ExperimentText() { | |||||
| height: '8px', | height: '8px', | ||||
| borderRadius: '50%', | borderRadius: '50%', | ||||
| marginRight: '6px', | marginRight: '6px', | ||||
| backgroundColor: experimentStatusInfo[message.status]?.color, | |||||
| backgroundColor: experimentStatusInfo[experimentIns?.status]?.color, | |||||
| }} | }} | ||||
| ></div> | ></div> | ||||
| <span style={{ color: experimentStatusInfo[message.status]?.color }}> | |||||
| {experimentStatusInfo[message.status]?.label} | |||||
| <span style={{ color: experimentStatusInfo[experimentIns?.status]?.color }}> | |||||
| {experimentStatusInfo[experimentIns?.status]?.label} | |||||
| </span> | </span> | ||||
| </div> | </div> | ||||
| <Button | <Button | ||||
| @@ -301,11 +425,24 @@ function ExperimentText() { | |||||
| </Button> | </Button> | ||||
| </div> | </div> | ||||
| <div className={styles['pipeline-container__graph']} ref={graphRef}></div> | <div className={styles['pipeline-container__graph']} ref={graphRef}></div> | ||||
| <Props ref={propsRef}></Props> | |||||
| {experimentNodeData ? ( | |||||
| <ExperimentDrawer | |||||
| open={propsDrawerOpen} | |||||
| onClose={closePropsDrawer} | |||||
| 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} | |||||
| ></ExperimentDrawer> | |||||
| ) : null} | |||||
| <ParamsModal | <ParamsModal | ||||
| open={paramsModalOpen} | open={paramsModalOpen} | ||||
| onCancel={closeParamsModal} | onCancel={closeParamsModal} | ||||
| globalParam={message.global_param} | |||||
| globalParam={experimentIns?.global_param} | |||||
| ></ParamsModal> | ></ParamsModal> | ||||
| </div> | </div> | ||||
| ); | ); | ||||
| @@ -30,4 +30,10 @@ | |||||
| background-image: url(/assets/images/pipeline-canvas-back.png); | background-image: url(/assets/images/pipeline-canvas-back.png); | ||||
| background-size: 100% 100%; | background-size: 100% 100%; | ||||
| } | } | ||||
| :global { | |||||
| .ant-drawer-mask { | |||||
| background: transparent !important; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -14,7 +14,12 @@ | |||||
| border: 1px solid #e0eaff; | border: 1px solid #e0eaff; | ||||
| } | } | ||||
| .ant-tabs-content-holder { | .ant-tabs-content-holder { | ||||
| overflow-y: auto; | |||||
| .ant-tabs-content { | |||||
| height: 100%; | |||||
| .ant-tabs-tabpane { | |||||
| height: 100%; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,11 +1,9 @@ | |||||
| import { getNodeResult, getQueryByExperimentLog } from '@/services/experiment/index.js'; | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { PipelineNodeModelSerialize } from '@/types'; | import { PipelineNodeModelSerialize } from '@/types'; | ||||
| import { elapsedTime, formatDate } from '@/utils/date'; | import { elapsedTime, formatDate } from '@/utils/date'; | ||||
| import { to } from '@/utils/promise'; | |||||
| import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; | 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 ExperimentParameter from '../components/ExperimentParameter'; | ||||
| import ExperimentResult from '../components/ExperimentResult'; | import ExperimentResult from '../components/ExperimentResult'; | ||||
| import LogList from '../components/LogList'; | import LogList from '../components/LogList'; | ||||
| @@ -19,154 +17,130 @@ export type ExperimentLog = { | |||||
| start_time?: string; // 日志开始时间 | start_time?: string; // 日志开始时间 | ||||
| }; | }; | ||||
| const Props = forwardRef((_, ref) => { | |||||
| const [form] = Form.useForm(); | |||||
| const [experimentNodeData, setExperimentNodeData] = useState<PipelineNodeModelSerialize>( | |||||
| {} as PipelineNodeModelSerialize, | |||||
| ); | |||||
| const [experimentResults, setExperimentResults] = useState([]); | |||||
| const [experimentLogList, setExperimentLogList] = useState<ExperimentLog[]>([]); | |||||
| 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: ( | |||||
| <LogList list={experimentLogList} status={experimentNodeData.experimentStatus}></LogList> | |||||
| ), | |||||
| icon: <ProfileOutlined />, | |||||
| }, | |||||
| { | |||||
| key: '2', | |||||
| label: '配置参数', | |||||
| icon: <DatabaseOutlined />, | |||||
| children: <ExperimentParameter form={form} nodeData={experimentNodeData} />, | |||||
| }, | |||||
| const ExperimentDrawer = forwardRef( | |||||
| ( | |||||
| { | { | ||||
| key: '3', | |||||
| label: '输出结果', | |||||
| children: <ExperimentResult results={experimentResults}></ExperimentResult>, | |||||
| icon: <ProfileOutlined />, | |||||
| }, | |||||
| ]; | |||||
| 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: ( | |||||
| <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, | |||||
| ], | |||||
| ); | |||||
| // 获取实验参数 | |||||
| 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 ( | |||||
| <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']}> | |||||
| 任务名称:{experimentNodeData.label} | |||||
| </div> | |||||
| <div className={styles['experiment-drawer__info']}> | |||||
| 执行状态: | |||||
| <div | |||||
| className={styles['experiment-drawer__status-dot']} | |||||
| style={{ | |||||
| backgroundColor: experimentStatusInfo[experimentNodeData.experimentStatus]?.color, | |||||
| }} | |||||
| ></div> | |||||
| <span style={{ color: experimentStatusInfo[experimentNodeData.experimentStatus]?.color }}> | |||||
| {experimentStatusInfo[experimentNodeData.experimentStatus]?.label} | |||||
| </span> | |||||
| </div> | |||||
| <div className={styles['experiment-drawer__info']}> | |||||
| 启动时间:{formatDate(experimentNodeData.experimentStartTime)} | |||||
| </div> | |||||
| <div className={styles['experiment-drawer__info']}> | |||||
| 耗时: | |||||
| {elapsedTime( | |||||
| experimentNodeData.experimentStartTime, | |||||
| experimentNodeData.experimentEndTime, | |||||
| )} | |||||
| 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> | </div> | ||||
| </div> | |||||
| <Tabs defaultActiveKey="1" items={items} className={styles['experiment-drawer__tabs']} /> | |||||
| </Drawer> | |||||
| ); | |||||
| }); | |||||
| <Tabs defaultActiveKey="1" items={items} className={styles['experiment-drawer__tabs']} /> | |||||
| </Drawer> | |||||
| ); | |||||
| }, | |||||
| ); | |||||
| export default Props; | |||||
| export default ExperimentDrawer; | |||||
| @@ -1,5 +1,7 @@ | |||||
| .experiment-parameter { | .experiment-parameter { | ||||
| height: 100%; | |||||
| padding-top: 8px; | padding-top: 8px; | ||||
| overflow-y: auto; | |||||
| &__title { | &__title { | ||||
| display: flex; | display: flex; | ||||
| @@ -3,16 +3,15 @@ import ParameterSelect from '@/components/ParameterSelect'; | |||||
| import SubAreaTitle from '@/components/SubAreaTitle'; | import SubAreaTitle from '@/components/SubAreaTitle'; | ||||
| import { useComputingResource } from '@/hooks/resource'; | import { useComputingResource } from '@/hooks/resource'; | ||||
| import { PipelineNodeModelSerialize } from '@/types'; | import { PipelineNodeModelSerialize } from '@/types'; | ||||
| import { Form, Input, Select, type FormProps } from 'antd'; | |||||
| import { Form, Input, Select } from 'antd'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| const { TextArea } = Input; | const { TextArea } = Input; | ||||
| type ExperimentParameterProps = { | type ExperimentParameterProps = { | ||||
| form: FormProps['form']; | |||||
| nodeData: PipelineNodeModelSerialize; | nodeData: PipelineNodeModelSerialize; | ||||
| }; | }; | ||||
| function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) { | |||||
| function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||||
| const [resourceStandardList] = useComputingResource(); // 资源规模 | const [resourceStandardList] = useComputingResource(); // 资源规模 | ||||
| // 控制策略 | // 控制策略 | ||||
| @@ -42,7 +41,7 @@ function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) { | |||||
| wrapperCol={{ | wrapperCol={{ | ||||
| span: 24, | span: 24, | ||||
| }} | }} | ||||
| form={form} | |||||
| initialValues={nodeData} | |||||
| style={{ | style={{ | ||||
| maxWidth: 600, | maxWidth: 600, | ||||
| }} | }} | ||||
| @@ -1,5 +1,7 @@ | |||||
| .experiment-result { | .experiment-result { | ||||
| height: 100%; | |||||
| padding: 8px; | padding: 8px; | ||||
| overflow-y: auto; | |||||
| color: @text-color; | color: @text-color; | ||||
| font-size: 14px; | font-size: 14px; | ||||
| @@ -1,11 +1,15 @@ | |||||
| import { getNodeResult } from '@/services/experiment/index.js'; | |||||
| import { downLoadZip } from '@/utils/downloadfile'; | import { downLoadZip } from '@/utils/downloadfile'; | ||||
| import { openAntdModal } from '@/utils/modal'; | import { openAntdModal } from '@/utils/modal'; | ||||
| import { to } from '@/utils/promise'; | |||||
| import { App, Button } from 'antd'; | import { App, Button } from 'antd'; | ||||
| import { useEffect, useState } from 'react'; | |||||
| import ExportModelModal from '../ExportModelModal'; | import ExportModelModal from '../ExportModelModal'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type ExperimentResultProps = { | type ExperimentResultProps = { | ||||
| results?: ExperimentResultData[] | null; | |||||
| experimentInsId?: number; // 实验实例 id | |||||
| pipelineNodeId?: string; // 流水线节点 id | |||||
| }; | }; | ||||
| type ExperimentResultData = { | type ExperimentResultData = { | ||||
| @@ -18,8 +22,21 @@ type ExperimentResultData = { | |||||
| }[]; | }[]; | ||||
| }; | }; | ||||
| function ExperimentResult({ results }: ExperimentResultProps) { | |||||
| function ExperimentResult({ experimentInsId, pipelineNodeId }: ExperimentResultProps) { | |||||
| const { message } = App.useApp(); | const { message } = App.useApp(); | ||||
| const [experimentResults, setExperimentResults] = useState<ExperimentResultData[]>([]); | |||||
| 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) => { | const download = (path: string) => { | ||||
| @@ -40,9 +57,9 @@ function ExperimentResult({ results }: ExperimentResultProps) { | |||||
| return ( | return ( | ||||
| <div className={styles['experiment-result']}> | <div className={styles['experiment-result']}> | ||||
| <div className={styles['experiment-result__content']}> | <div className={styles['experiment-result__content']}> | ||||
| {results && results.length > 0 ? ( | |||||
| results.map((item) => ( | |||||
| <div key={item.name} className={styles['experiment-result__item']}> | |||||
| {experimentResults.length > 0 ? ( | |||||
| experimentResults.map((item) => ( | |||||
| <div key={item.name || item.path} className={styles['experiment-result__item']}> | |||||
| <div className={styles['experiment-result__item__name']}> | <div className={styles['experiment-result__item__name']}> | ||||
| <span>{item.name}</span> | <span>{item.name}</span> | ||||
| <Button | <Button | ||||
| @@ -11,11 +11,11 @@ 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 { useEffect, useState } from 'react'; | |||||
| import { useEffect, useRef, useState } from 'react'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| export type LogGroupProps = ExperimentLog & { | export type LogGroupProps = ExperimentLog & { | ||||
| status: ExperimentStatus; // 实验状态 | |||||
| status?: ExperimentStatus; // 实验状态 | |||||
| }; | }; | ||||
| type Log = { | type Log = { | ||||
| @@ -25,7 +25,7 @@ type Log = { | |||||
| // 滚动到底部 | // 滚动到底部 | ||||
| const scrollToBottom = (smooth: boolean = true) => { | const scrollToBottom = (smooth: boolean = true) => { | ||||
| const element = document.getElementsByClassName('ant-tabs-content-holder')?.[0]; | |||||
| const element = document.getElementById('log-list'); | |||||
| if (element) { | if (element) { | ||||
| const optons: ScrollToOptions = { | const optons: ScrollToOptions = { | ||||
| top: element.scrollHeight, | top: element.scrollHeight, | ||||
| @@ -41,25 +41,36 @@ function LogGroup({ | |||||
| pod_name = '', | pod_name = '', | ||||
| log_content = '', | log_content = '', | ||||
| start_time, | start_time, | ||||
| status = ExperimentStatus.Pending, | |||||
| status, | |||||
| }: LogGroupProps) { | }: LogGroupProps) { | ||||
| const [collapse, setCollapse] = useState(true); | const [collapse, setCollapse] = useState(true); | ||||
| const [logList, setLogList, logListRef] = useStateRef<Log[]>([]); | const [logList, setLogList, logListRef] = useStateRef<Log[]>([]); | ||||
| const [completed, setCompleted] = useState(false); | const [completed, setCompleted] = useState(false); | ||||
| // 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); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| scrollToBottom(false); | scrollToBottom(false); | ||||
| let timerId: NodeJS.Timeout | undefined; | |||||
| if (status === ExperimentStatus.Running) { | if (status === ExperimentStatus.Running) { | ||||
| const timerId = setInterval(() => { | |||||
| timerId = setInterval(() => { | |||||
| requestExperimentPodsLog(); | requestExperimentPodsLog(); | ||||
| }, 5000); | |||||
| return () => { | |||||
| clearInterval(timerId); | |||||
| }; | |||||
| }, 5 * 1000); | |||||
| } else if (preStatusRef.current === ExperimentStatus.Running) { | |||||
| requestExperimentPodsLog(); | |||||
| setTimeout(() => { | |||||
| requestExperimentPodsLog(); | |||||
| }, 5 * 1000); | |||||
| } | } | ||||
| }, []); | |||||
| preStatusRef.current = status; | |||||
| return () => { | |||||
| if (timerId) { | |||||
| clearInterval(timerId); | |||||
| timerId = undefined; | |||||
| } | |||||
| }; | |||||
| }, [status]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const mouseDown = () => { | const mouseDown = () => { | ||||
| @@ -131,7 +142,8 @@ function LogGroup({ | |||||
| 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 = status !== 'Running' && showLog && !completed && logText !== ''; | |||||
| const showMoreBtn = | |||||
| status !== ExperimentStatus.Running && showLog && !completed && logText !== ''; | |||||
| return ( | return ( | ||||
| <div className={styles['log-group']}> | <div className={styles['log-group']}> | ||||
| {log_type === 'resource' && ( | {log_type === 'resource' && ( | ||||
| @@ -1,5 +1,7 @@ | |||||
| .log-list { | .log-list { | ||||
| height: 100%; | |||||
| padding: 8px; | padding: 8px; | ||||
| overflow-y: auto; | |||||
| &__empty { | &__empty { | ||||
| padding: 15px; | padding: 15px; | ||||
| @@ -1,18 +1,74 @@ | |||||
| import { ExperimentStatus } from '@/enums'; | import { ExperimentStatus } from '@/enums'; | ||||
| import { ExperimentLog } from '@/pages/Experiment/Info/props'; | 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 LogGroup from '../LogGroup'; | import LogGroup from '../LogGroup'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type LogListProps = { | type LogListProps = { | ||||
| list: ExperimentLog[]; | |||||
| status: ExperimentStatus; | |||||
| instanceName?: string; // 实验实例 name | |||||
| instanceNamespace?: string; // 实验实例 namespace | |||||
| pipelineNodeId?: string; // 流水线节点 id | |||||
| workflowId?: string; // 实验实例工作流 id | |||||
| instanceNodeStartTime?: string; // 实验实例节点开始运行时间 | |||||
| instanceNodeStatus?: ExperimentStatus; | |||||
| }; | }; | ||||
| function LogList({ list = [], status }: LogListProps) { | |||||
| function LogList({ | |||||
| instanceName, | |||||
| instanceNamespace, | |||||
| pipelineNodeId, | |||||
| workflowId, | |||||
| instanceNodeStartTime, | |||||
| instanceNodeStatus, | |||||
| }: LogListProps) { | |||||
| const [logList, setLogList] = useState<ExperimentLog[]>([]); | |||||
| 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); | |||||
| } | |||||
| }, [workflowId, instanceNodeStartTime]); | |||||
| // 获取实验日志 | |||||
| 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, | |||||
| }, | |||||
| ]; | |||||
| setLogList(list); | |||||
| } else if (log_type === 'resource') { | |||||
| const list = pods.map((v: string) => ({ | |||||
| log_type, | |||||
| pod_name: v, | |||||
| log_content: '', | |||||
| start_time, | |||||
| })); | |||||
| setLogList(list); | |||||
| } | |||||
| } | |||||
| }; | |||||
| return ( | return ( | ||||
| <div className={styles['log-list']}> | |||||
| {list.length > 0 ? ( | |||||
| list.map((v) => <LogGroup key={v.pod_name} {...v} status={status} />) | |||||
| <div className={styles['log-list']} id="log-list"> | |||||
| {logList.length > 0 ? ( | |||||
| logList.map((v) => <LogGroup key={v.pod_name} {...v} status={instanceNodeStatus} />) | |||||
| ) : ( | ) : ( | ||||
| <div className={styles['log-list__empty']}>暂无日志</div> | <div className={styles['log-list__empty']}>暂无日志</div> | ||||
| )} | )} | ||||
| @@ -66,6 +66,7 @@ function Experiment() { | |||||
| clearExperimentInTimers(); | clearExperimentInTimers(); | ||||
| }; | }; | ||||
| }, []); | }, []); | ||||
| // 获取实验列表 | // 获取实验列表 | ||||
| const getList = async () => { | const getList = async () => { | ||||
| const params = { | const params = { | ||||
| @@ -84,6 +85,7 @@ function Experiment() { | |||||
| setTotal(res.data.totalElements); | setTotal(res.data.totalElements); | ||||
| } | } | ||||
| }; | }; | ||||
| // 获取流水线列表 | // 获取流水线列表 | ||||
| const getWorkflowList = async () => { | const getWorkflowList = async () => { | ||||
| const [res, _] = await to(getWorkflow(queryFlow)); | const [res, _] = await to(getWorkflow(queryFlow)); | ||||
| @@ -91,6 +93,7 @@ function Experiment() { | |||||
| setWorkflowList(res.data.content); | setWorkflowList(res.data.content); | ||||
| } | } | ||||
| }; | }; | ||||
| // 获取实验实例列表 | // 获取实验实例列表 | ||||
| const getQueryByExperiment = async (experimentId, page) => { | const getQueryByExperiment = async (experimentId, page) => { | ||||
| const params = { | const params = { | ||||
| @@ -128,6 +131,7 @@ function Experiment() { | |||||
| } | } | ||||
| } | } | ||||
| }; | }; | ||||
| // 运行 TensorBoard | // 运行 TensorBoard | ||||
| const runTensorBoard = async (experimentIn) => { | const runTensorBoard = async (experimentIn) => { | ||||
| const params = { | const params = { | ||||
| @@ -146,6 +150,7 @@ function Experiment() { | |||||
| } | } | ||||
| } | } | ||||
| }; | }; | ||||
| // 获取 TensorBoard 状态 | // 获取 TensorBoard 状态 | ||||
| const getTensorBoardStatus = async (experimentIn) => { | const getTensorBoardStatus = async (experimentIn) => { | ||||
| const params = { | const params = { | ||||
| @@ -179,6 +184,7 @@ function Experiment() { | |||||
| timerIds.set(experimentIn.id, timerId); | timerIds.set(experimentIn.id, timerId); | ||||
| } | } | ||||
| }; | }; | ||||
| // 展开实例 | // 展开实例 | ||||
| const expandChange = (e, record) => { | const expandChange = (e, record) => { | ||||
| clearExperimentInTimers(); | clearExperimentInTimers(); | ||||
| @@ -189,6 +195,7 @@ function Experiment() { | |||||
| getQueryByExperiment(record.id, 0); | getQueryByExperiment(record.id, 0); | ||||
| } | } | ||||
| }; | }; | ||||
| // 终止实验实例获取 TensorBoard 状态的定时器 | // 终止实验实例获取 TensorBoard 状态的定时器 | ||||
| const clearExperimentInTimers = () => { | const clearExperimentInTimers = () => { | ||||
| timerIds.values().forEach((timerId) => { | timerIds.values().forEach((timerId) => { | ||||
| @@ -196,6 +203,7 @@ function Experiment() { | |||||
| }); | }); | ||||
| timerIds.clear(); | timerIds.clear(); | ||||
| }; | }; | ||||
| // 创建实验 | // 创建实验 | ||||
| const createExperiment = () => { | const createExperiment = () => { | ||||
| setIsAdd(true); | setIsAdd(true); | ||||
| @@ -203,6 +211,7 @@ function Experiment() { | |||||
| setExperimentId(null); | setExperimentId(null); | ||||
| setIsModalOpen(true); | setIsModalOpen(true); | ||||
| }; | }; | ||||
| // 编辑实验 | // 编辑实验 | ||||
| const editExperiment = (id) => { | const editExperiment = (id) => { | ||||
| getExperimentById(id).then((res) => { | getExperimentById(id).then((res) => { | ||||
| @@ -218,11 +227,7 @@ function Experiment() { | |||||
| const handleCancel = () => { | const handleCancel = () => { | ||||
| setIsModalOpen(false); | setIsModalOpen(false); | ||||
| }; | }; | ||||
| // 跳转到流水线 | |||||
| const routeToEdit = (e, record) => { | |||||
| e.stopPropagation(); | |||||
| navgite({ pathname: `/pipeline/template/${record.workflow_id}` }); | |||||
| }; | |||||
| // 创建或者编辑实验接口请求 | // 创建或者编辑实验接口请求 | ||||
| const handleAddExperiment = async (values) => { | const handleAddExperiment = async (values) => { | ||||
| const global_param = JSON.stringify(values.global_param); | const global_param = JSON.stringify(values.global_param); | ||||
| @@ -266,6 +271,13 @@ function Experiment() { | |||||
| message.error('运行失败'); | message.error('运行失败'); | ||||
| } | } | ||||
| }; | }; | ||||
| // 跳转到流水线 | |||||
| const gotoPipeline = (e, record) => { | |||||
| e.stopPropagation(); | |||||
| navgite({ pathname: `/pipeline/template/${record.workflow_id}` }); | |||||
| }; | |||||
| // 跳转到实验实例详情 | // 跳转到实验实例详情 | ||||
| const gotoInstanceInfo = (item, record) => { | const gotoInstanceInfo = (item, record) => { | ||||
| navgite({ pathname: `/pipeline/experiment/${record.workflow_id}/${item.id}` }); | navgite({ pathname: `/pipeline/experiment/${record.workflow_id}/${item.id}` }); | ||||
| @@ -343,7 +355,7 @@ function Experiment() { | |||||
| title: '关联流水线名称', | title: '关联流水线名称', | ||||
| dataIndex: 'workflow_name', | dataIndex: 'workflow_name', | ||||
| key: 'workflow_name', | key: 'workflow_name', | ||||
| render: (text, record) => <a onClick={(e) => routeToEdit(e, record)}>{text}</a>, | |||||
| render: (text, record) => <a onClick={(e) => gotoPipeline(e, record)}>{text}</a>, | |||||
| width: '16%', | width: '16%', | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -47,6 +47,7 @@ export type PipelineNodeModel = { | |||||
| out_parameters: string; | out_parameters: string; | ||||
| component_label: string; | component_label: string; | ||||
| icon_path: string; | icon_path: string; | ||||
| workflowId?: string; | |||||
| }; | }; | ||||
| // 流水线节点模型数据 | // 流水线节点模型数据 | ||||