| @@ -32,6 +32,7 @@ export function useStateRef<T>(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<T> = (state: T) => void; | |||
| @@ -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() { | |||
| <div className={styles['pipeline-container']}> | |||
| <div className={styles['pipeline-container__top']}> | |||
| <div className={styles['pipeline-container__top__info']}> | |||
| 启动时间:{formatDate(message.create_time)} | |||
| 启动时间:{formatDate(experimentIns?.create_time)} | |||
| </div> | |||
| <div className={styles['pipeline-container__top__info']}> | |||
| 执行时长: | |||
| {elapsedTime(message.create_time, message.finish_time)} | |||
| {elapsedTime(experimentIns?.create_time, experimentIns?.finish_time)} | |||
| </div> | |||
| <div className={styles['pipeline-container__top__info']}> | |||
| 状态: | |||
| @@ -286,11 +410,11 @@ function ExperimentText() { | |||
| height: '8px', | |||
| borderRadius: '50%', | |||
| marginRight: '6px', | |||
| backgroundColor: experimentStatusInfo[message.status]?.color, | |||
| backgroundColor: experimentStatusInfo[experimentIns?.status]?.color, | |||
| }} | |||
| ></div> | |||
| <span style={{ color: experimentStatusInfo[message.status]?.color }}> | |||
| {experimentStatusInfo[message.status]?.label} | |||
| <span style={{ color: experimentStatusInfo[experimentIns?.status]?.color }}> | |||
| {experimentStatusInfo[experimentIns?.status]?.label} | |||
| </span> | |||
| </div> | |||
| <Button | |||
| @@ -301,11 +425,24 @@ function ExperimentText() { | |||
| </Button> | |||
| </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 | |||
| open={paramsModalOpen} | |||
| onCancel={closeParamsModal} | |||
| globalParam={message.global_param} | |||
| globalParam={experimentIns?.global_param} | |||
| ></ParamsModal> | |||
| </div> | |||
| ); | |||
| @@ -30,4 +30,10 @@ | |||
| background-image: url(/assets/images/pipeline-canvas-back.png); | |||
| background-size: 100% 100%; | |||
| } | |||
| :global { | |||
| .ant-drawer-mask { | |||
| background: transparent !important; | |||
| } | |||
| } | |||
| } | |||
| @@ -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%; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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<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> | |||
| <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 { | |||
| height: 100%; | |||
| padding-top: 8px; | |||
| overflow-y: auto; | |||
| &__title { | |||
| display: flex; | |||
| @@ -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, | |||
| }} | |||
| @@ -1,5 +1,7 @@ | |||
| .experiment-result { | |||
| height: 100%; | |||
| padding: 8px; | |||
| overflow-y: auto; | |||
| color: @text-color; | |||
| font-size: 14px; | |||
| @@ -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<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) => { | |||
| @@ -40,9 +57,9 @@ function ExperimentResult({ results }: ExperimentResultProps) { | |||
| return ( | |||
| <div className={styles['experiment-result']}> | |||
| <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']}> | |||
| <span>{item.name}</span> | |||
| <Button | |||
| @@ -11,11 +11,11 @@ import { getExperimentPodsLog } from '@/services/experiment/index.js'; | |||
| import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons'; | |||
| import { Button } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| export type LogGroupProps = ExperimentLog & { | |||
| status: ExperimentStatus; // 实验状态 | |||
| status?: ExperimentStatus; // 实验状态 | |||
| }; | |||
| type Log = { | |||
| @@ -25,7 +25,7 @@ type Log = { | |||
| // 滚动到底部 | |||
| const scrollToBottom = (smooth: boolean = true) => { | |||
| const element = document.getElementsByClassName('ant-tabs-content-holder')?.[0]; | |||
| const element = document.getElementById('log-list'); | |||
| if (element) { | |||
| const optons: ScrollToOptions = { | |||
| top: element.scrollHeight, | |||
| @@ -41,25 +41,36 @@ function LogGroup({ | |||
| pod_name = '', | |||
| log_content = '', | |||
| start_time, | |||
| status = ExperimentStatus.Pending, | |||
| status, | |||
| }: LogGroupProps) { | |||
| const [collapse, setCollapse] = useState(true); | |||
| const [logList, setLogList, logListRef] = useStateRef<Log[]>([]); | |||
| const [completed, setCompleted] = useState(false); | |||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||
| const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | |||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | |||
| useEffect(() => { | |||
| scrollToBottom(false); | |||
| let timerId: NodeJS.Timeout | undefined; | |||
| if (status === ExperimentStatus.Running) { | |||
| const timerId = setInterval(() => { | |||
| timerId = setInterval(() => { | |||
| 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(() => { | |||
| const mouseDown = () => { | |||
| @@ -131,7 +142,8 @@ function LogGroup({ | |||
| const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; | |||
| 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 ( | |||
| <div className={styles['log-group']}> | |||
| {log_type === 'resource' && ( | |||
| @@ -1,5 +1,7 @@ | |||
| .log-list { | |||
| height: 100%; | |||
| padding: 8px; | |||
| overflow-y: auto; | |||
| &__empty { | |||
| padding: 15px; | |||
| @@ -1,18 +1,74 @@ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { ExperimentLog } from '@/pages/Experiment/Info/props'; | |||
| import { getQueryByExperimentLog } from '@/services/experiment/index.js'; | |||
| import { to } from '@/utils/promise'; | |||
| import dayjs from 'dayjs'; | |||
| import { useEffect, useState } from 'react'; | |||
| import LogGroup from '../LogGroup'; | |||
| import styles from './index.less'; | |||
| 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 ( | |||
| <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> | |||
| )} | |||
| @@ -66,6 +66,7 @@ function Experiment() { | |||
| clearExperimentInTimers(); | |||
| }; | |||
| }, []); | |||
| // 获取实验列表 | |||
| const getList = async () => { | |||
| const params = { | |||
| @@ -84,6 +85,7 @@ function Experiment() { | |||
| setTotal(res.data.totalElements); | |||
| } | |||
| }; | |||
| // 获取流水线列表 | |||
| const getWorkflowList = async () => { | |||
| const [res, _] = await to(getWorkflow(queryFlow)); | |||
| @@ -91,6 +93,7 @@ function Experiment() { | |||
| setWorkflowList(res.data.content); | |||
| } | |||
| }; | |||
| // 获取实验实例列表 | |||
| const getQueryByExperiment = async (experimentId, page) => { | |||
| const params = { | |||
| @@ -128,6 +131,7 @@ function Experiment() { | |||
| } | |||
| } | |||
| }; | |||
| // 运行 TensorBoard | |||
| const runTensorBoard = async (experimentIn) => { | |||
| const params = { | |||
| @@ -146,6 +150,7 @@ function Experiment() { | |||
| } | |||
| } | |||
| }; | |||
| // 获取 TensorBoard 状态 | |||
| const getTensorBoardStatus = async (experimentIn) => { | |||
| const params = { | |||
| @@ -179,6 +184,7 @@ function Experiment() { | |||
| timerIds.set(experimentIn.id, timerId); | |||
| } | |||
| }; | |||
| // 展开实例 | |||
| const expandChange = (e, record) => { | |||
| clearExperimentInTimers(); | |||
| @@ -189,6 +195,7 @@ function Experiment() { | |||
| getQueryByExperiment(record.id, 0); | |||
| } | |||
| }; | |||
| // 终止实验实例获取 TensorBoard 状态的定时器 | |||
| const clearExperimentInTimers = () => { | |||
| timerIds.values().forEach((timerId) => { | |||
| @@ -196,6 +203,7 @@ function Experiment() { | |||
| }); | |||
| timerIds.clear(); | |||
| }; | |||
| // 创建实验 | |||
| const createExperiment = () => { | |||
| setIsAdd(true); | |||
| @@ -203,6 +211,7 @@ function Experiment() { | |||
| setExperimentId(null); | |||
| setIsModalOpen(true); | |||
| }; | |||
| // 编辑实验 | |||
| const editExperiment = (id) => { | |||
| getExperimentById(id).then((res) => { | |||
| @@ -218,11 +227,7 @@ function Experiment() { | |||
| const handleCancel = () => { | |||
| setIsModalOpen(false); | |||
| }; | |||
| // 跳转到流水线 | |||
| const routeToEdit = (e, record) => { | |||
| e.stopPropagation(); | |||
| navgite({ pathname: `/pipeline/template/${record.workflow_id}` }); | |||
| }; | |||
| // 创建或者编辑实验接口请求 | |||
| const handleAddExperiment = async (values) => { | |||
| const global_param = JSON.stringify(values.global_param); | |||
| @@ -266,6 +271,13 @@ function Experiment() { | |||
| message.error('运行失败'); | |||
| } | |||
| }; | |||
| // 跳转到流水线 | |||
| const gotoPipeline = (e, record) => { | |||
| e.stopPropagation(); | |||
| navgite({ pathname: `/pipeline/template/${record.workflow_id}` }); | |||
| }; | |||
| // 跳转到实验实例详情 | |||
| const gotoInstanceInfo = (item, record) => { | |||
| navgite({ pathname: `/pipeline/experiment/${record.workflow_id}/${item.id}` }); | |||
| @@ -343,7 +355,7 @@ function Experiment() { | |||
| title: '关联流水线名称', | |||
| dataIndex: '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%', | |||
| }, | |||
| { | |||
| @@ -47,6 +47,7 @@ export type PipelineNodeModel = { | |||
| out_parameters: string; | |||
| component_label: string; | |||
| icon_path: string; | |||
| workflowId?: string; | |||
| }; | |||
| // 流水线节点模型数据 | |||