| @@ -6,8 +6,6 @@ export default async () => { | |||
| target: 'browser', | |||
| }), | |||
| }); | |||
| console.log(); | |||
| return { | |||
| ...config, | |||
| testEnvironmentOptions: { | |||
| @@ -32,7 +32,7 @@ | |||
| "record": "cross-env NODE_ENV=development REACT_APP_ENV=test max record --scene=login", | |||
| "serve": "umi-serve", | |||
| "start": "cross-env UMI_ENV=dev max dev", | |||
| "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev", | |||
| "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev UMI_DEV_SERVER_COMPRESS=none max dev", | |||
| "start:mock": "cross-env REACT_APP_ENV=dev UMI_ENV=dev max dev", | |||
| "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev", | |||
| "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev", | |||
| @@ -34,9 +34,7 @@ export async function getInitialState(): Promise<{ | |||
| // console.log('getInitialState'); | |||
| const fetchUserInfo = async () => { | |||
| try { | |||
| const response = await getUserInfo({ | |||
| skipErrorHandler: true, | |||
| }); | |||
| const response = await getUserInfo(); | |||
| return { | |||
| ...response.user, | |||
| avatar: response.user.avatar || require('@/assets/img/avatar-default.png'), | |||
| @@ -45,7 +43,7 @@ export async function getInitialState(): Promise<{ | |||
| roleNames: response.user.roles, | |||
| } as API.CurrentUser; | |||
| } catch (error) { | |||
| console.log(error); | |||
| console.error(error); | |||
| gotoLoginPage(); | |||
| } | |||
| return undefined; | |||
| @@ -63,7 +63,7 @@ const DictTag: React.FC<DictTagProps> = (props) => { | |||
| } | |||
| if (props.options) { | |||
| if (!Array.isArray(props.options)) { | |||
| console.log('DictTag options is no array!'); | |||
| // console.log('DictTag options is no array!'); | |||
| return ''; | |||
| } | |||
| for (const item of props.options) { | |||
| @@ -85,7 +85,7 @@ const DictTag: React.FC<DictTagProps> = (props) => { | |||
| } | |||
| if (props.options) { | |||
| if (!Array.isArray(props.options)) { | |||
| console.log('DictTag options is no array!'); | |||
| // console.log('DictTag options is no array!'); | |||
| return 'default'; | |||
| } | |||
| for (const item of props.options) { | |||
| @@ -15,7 +15,7 @@ const clearCache = () => { | |||
| caches.delete(key); | |||
| }); | |||
| }) | |||
| .catch((e) => console.log(e)); | |||
| .catch((e) => console.error(e)); | |||
| } | |||
| }; | |||
| @@ -94,7 +94,7 @@ function ExperimentComparison() { | |||
| fixed: 'left', | |||
| selectedRowKeys, | |||
| onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => { | |||
| console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); | |||
| // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); | |||
| setSelectedRowKeys(selectedRowKeys); | |||
| }, | |||
| }; | |||
| @@ -10,10 +10,10 @@ import G6, { Util } from '@antv/g6'; | |||
| import { Button } from 'antd'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| import { useNavigate, useParams } from 'react-router-dom'; | |||
| import ExperimentDrawer from '../components/ExperimentDrawer'; | |||
| import ParamsModal from '../components/ViewParamsModal'; | |||
| import { experimentStatusInfo } from '../status'; | |||
| import styles from './index.less'; | |||
| import ExperimentDrawer from './props'; | |||
| let graph = null; | |||
| @@ -28,6 +28,7 @@ function ExperimentText() { | |||
| const [propsDrawerOpen, openPropsDrawer, closePropsDrawer, propsDrawerOpenRef] = | |||
| useVisible(false); | |||
| const navigate = useNavigate(); | |||
| const evtSourceRef = useRef(); | |||
| const width = 110; | |||
| const height = 36; | |||
| @@ -48,6 +49,10 @@ function ExperimentText() { | |||
| if (timerRef.current) { | |||
| clearTimeout(timerRef.current); | |||
| } | |||
| if (evtSourceRef.current) { | |||
| evtSourceRef.current.close(); | |||
| evtSourceRef.current = null; | |||
| } | |||
| }; | |||
| }, []); | |||
| @@ -68,57 +73,39 @@ function ExperimentText() { | |||
| item.imgName = item.img.slice(0, item.img.length - 4); | |||
| }); | |||
| workflowRef.current = dag; | |||
| getExperimentInstance(true); | |||
| getExperimentInstance(); | |||
| } catch (error) { | |||
| // JSON.parse 错误 | |||
| console.log(error); | |||
| console.error('JSON.parse error: ', error); | |||
| } | |||
| } | |||
| }; | |||
| // 获取实验实例 | |||
| const getExperimentInstance = async (first) => { | |||
| const getExperimentInstance = async () => { | |||
| const [res] = await to(getExperimentIns(locationParams.id)); | |||
| if (res && res.data && workflowRef.current) { | |||
| setExperimentIns(res.data); | |||
| const { status, nodes_status } = res.data; | |||
| const { status, nodes_status, argo_ins_ns, argo_ins_name } = res.data; | |||
| const workflowData = workflowRef.current; | |||
| const experimentStatusObjs = JSON.parse(nodes_status); | |||
| workflowData.nodes.forEach((item) => { | |||
| const experimentNode = experimentStatusObjs?.[item.id] ?? {}; | |||
| const { finishedAt, startedAt, phase, id } = experimentNode; | |||
| item.experimentStartTime = startedAt; | |||
| item.experimentEndTime = finishedAt; | |||
| item.experimentStatus = phase; | |||
| item.workflowId = id; | |||
| item.img = phase ? `${item.imgName}-${phase}.png` : `${item.imgName}.png`; | |||
| const experimentNode = experimentStatusObjs?.[item.id]; | |||
| updateWorkflowNode(item, experimentNode); | |||
| }); | |||
| // 更新打开的抽屉数据 | |||
| if (propsDrawerOpenRef.current && experimentNodeDataRef.current) { | |||
| const currentId = experimentNodeDataRef.current.id; | |||
| const node = workflowData.nodes.find((item) => item.id === currentId); | |||
| if (node) { | |||
| setExperimentNodeData(node); | |||
| } | |||
| } | |||
| getGraphData(workflowData, first); | |||
| // 运行中或者等待中,每5秒获取一次实验实例 | |||
| if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { | |||
| timerRef.current = setTimeout(() => { | |||
| getExperimentInstance(false); | |||
| }, 5 * 1000); | |||
| } | |||
| // 绘制图 | |||
| getGraphData(workflowData, true); | |||
| if (first && status === ExperimentStatus.Pending) { | |||
| if (status === ExperimentStatus.Pending) { | |||
| // 如果状态是 Pending, 打开第一个节点 | |||
| const node = workflowData.nodes[0]; | |||
| if (node) { | |||
| setExperimentNodeData(node); | |||
| openPropsDrawer(); | |||
| } | |||
| } else if (first && status === ExperimentStatus.Running) { | |||
| } else if (status === ExperimentStatus.Running) { | |||
| // 如果状态是 Running,打开第一个运行中的节点,如果没有运行中的节点,则打开第一个节点 | |||
| const node = | |||
| workflowData.nodes.find((item) => item.experimentStatus === ExperimentStatus.Running) ?? | |||
| workflowData.nodes[0]; | |||
| @@ -127,9 +114,83 @@ function ExperimentText() { | |||
| openPropsDrawer(); | |||
| } | |||
| } | |||
| // 运行中或者等待中,开启 SSE | |||
| if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { | |||
| setupSSE(argo_ins_name, argo_ins_ns); | |||
| } | |||
| } | |||
| }; | |||
| const setupSSE = (name, namespace) => { | |||
| const { origin } = location; | |||
| const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); | |||
| const evtSource = new EventSource( | |||
| `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, | |||
| { withCredentials: false }, | |||
| ); | |||
| evtSource.onmessage = (event) => { | |||
| const data = event?.data; | |||
| if (!data) { | |||
| return; | |||
| } | |||
| try { | |||
| const dataJson = JSON.parse(data); | |||
| const statusData = dataJson?.result?.object?.status; | |||
| if (!statusData) { | |||
| return; | |||
| } | |||
| const { startedAt, finishedAt, phase, nodes = {} } = statusData; | |||
| setExperimentIns((prev) => ({ | |||
| ...prev, | |||
| finish_time: finishedAt, | |||
| status: phase, | |||
| })); | |||
| const workflowData = workflowRef.current; | |||
| workflowData.nodes.forEach((item) => { | |||
| const experimentNode = Object.values(nodes).find((node) => node.displayName === item.id); | |||
| updateWorkflowNode(item, experimentNode); | |||
| }); | |||
| getGraphData(workflowData, false); | |||
| // 更新打开的抽屉数据 | |||
| if (propsDrawerOpenRef.current && experimentNodeDataRef.current) { | |||
| const currentId = experimentNodeDataRef.current.id; | |||
| const node = workflowData.nodes.find((item) => item.id === currentId); | |||
| if (node) { | |||
| setExperimentNodeData(node); | |||
| } | |||
| } | |||
| if (phase !== ExperimentStatus.Pending && phase !== ExperimentStatus.Running) { | |||
| evtSource.close(); | |||
| evtSourceRef.current = null; | |||
| } | |||
| } catch (error) { | |||
| console.error('JSON.parse error: ', error); | |||
| } | |||
| }; | |||
| evtSource.onerror = (error) => { | |||
| console.error('SSE error: ', error); | |||
| }; | |||
| evtSourceRef.current = evtSource; | |||
| }; | |||
| function updateWorkflowNode(workflowNode, statusNode) { | |||
| if (!statusNode) { | |||
| return; | |||
| } | |||
| const { finishedAt, startedAt, phase, id } = statusNode; | |||
| workflowNode.experimentStartTime = startedAt; | |||
| workflowNode.experimentEndTime = finishedAt; | |||
| workflowNode.experimentStatus = phase; | |||
| workflowNode.workflowId = id; | |||
| workflowNode.img = phase | |||
| ? `${workflowNode.imgName}-${phase}.png` | |||
| : `${workflowNode.imgName}.png`; | |||
| } | |||
| // 根据数据,渲染图 | |||
| const getGraphData = (data, first) => { | |||
| if (graph) { | |||
| @@ -149,7 +210,7 @@ function ExperimentText() { | |||
| } | |||
| } else { | |||
| setTimeout(() => { | |||
| getGraphData(data); | |||
| getGraphData(data, first); | |||
| }, 500); | |||
| } | |||
| }; | |||
| @@ -390,6 +451,12 @@ function ExperimentText() { | |||
| graph.on('node:mouseleave', (e) => { | |||
| graph.setItemState(e.item, 'hover', false); | |||
| }); | |||
| graph.on('canvas:click', (e) => { | |||
| closePropsDrawer(); | |||
| setTimeout(() => { | |||
| setExperimentNodeData(null); | |||
| }, 200); | |||
| }); | |||
| }; | |||
| return ( | |||
| @@ -425,18 +492,19 @@ function ExperimentText() { | |||
| </Button> | |||
| </div> | |||
| <div className={styles['pipeline-container__graph']} ref={graphRef}></div> | |||
| {experimentNodeData ? ( | |||
| {experimentIns && experimentNodeData ? ( | |||
| <ExperimentDrawer | |||
| key={experimentNodeData.id} | |||
| open={propsDrawerOpen} | |||
| onClose={closePropsDrawer} | |||
| instanceId={experimentIns?.id} | |||
| instanceName={experimentIns?.argo_ins_name} | |||
| instanceNamespace={experimentIns?.argo_ins_ns} | |||
| instanceId={experimentIns.id} | |||
| instanceName={experimentIns.argo_ins_name} | |||
| instanceNamespace={experimentIns.argo_ins_ns} | |||
| instanceNodeData={experimentNodeData} | |||
| workflowId={experimentNodeData.workflowId} | |||
| instanceNodeStatus={experimentNodeData.experimentStatus} | |||
| instanceNodeStartTime={experimentNodeData.experimentStartTime} | |||
| instanceNodeEndTime={experimentIns.experimentEndTime} | |||
| instanceNodeEndTime={experimentNodeData.experimentEndTime} | |||
| ></ExperimentDrawer> | |||
| ) : null} | |||
| <ParamsModal | |||
| @@ -1,146 +0,0 @@ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { PipelineNodeModelSerialize } from '@/types'; | |||
| import { elapsedTime, formatDate } from '@/utils/date'; | |||
| import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; | |||
| import { Drawer, Tabs } from 'antd'; | |||
| import { forwardRef, useImperativeHandle, useMemo } from 'react'; | |||
| import ExperimentParameter from '../components/ExperimentParameter'; | |||
| import ExperimentResult from '../components/ExperimentResult'; | |||
| import LogList from '../components/LogList'; | |||
| import { experimentStatusInfo } from '../status'; | |||
| import styles from './props.less'; | |||
| export type ExperimentLog = { | |||
| log_type: 'normal' | 'resource'; // 日志类型 | |||
| pod_name?: string; // 分布式名称 | |||
| log_content?: string; // 日志内容 | |||
| start_time?: string; // 日志开始时间 | |||
| }; | |||
| type ExperimentDrawerProps = { | |||
| open: boolean; | |||
| onClose: () => void; | |||
| instanceId?: number; // 实验实例 id | |||
| instanceName?: string; // 实验实例 name | |||
| instanceNamespace?: string; // 实验实例 namespace | |||
| instanceNodeData: PipelineNodeModelSerialize; // 节点数据,在定时刷新实验实例状态中不会变化 | |||
| workflowId?: string; // 实验实例工作流 id | |||
| instanceNodeStatus?: ExperimentStatus; // 在定时刷新实验实例状态中,变化一两次 | |||
| instanceNodeStartTime?: string; // 在定时刷新实验实例状态中,变化一两次 | |||
| instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化 | |||
| }; | |||
| const ExperimentDrawer = forwardRef( | |||
| ( | |||
| { | |||
| open, | |||
| onClose, | |||
| instanceId, | |||
| instanceName, | |||
| instanceNamespace, | |||
| instanceNodeData, | |||
| workflowId, | |||
| instanceNodeStatus, | |||
| instanceNodeStartTime, | |||
| instanceNodeEndTime, | |||
| }: ExperimentDrawerProps, | |||
| ref, | |||
| ) => { | |||
| useImperativeHandle(ref, () => ({})); | |||
| // 如果性能有问题,可以进一步拆解 | |||
| const items = useMemo( | |||
| () => [ | |||
| { | |||
| key: '1', | |||
| label: '日志详情', | |||
| children: ( | |||
| <LogList | |||
| instanceName={instanceName} | |||
| instanceNamespace={instanceNamespace} | |||
| pipelineNodeId={instanceNodeData.id} | |||
| workflowId={workflowId} | |||
| instanceNodeStartTime={instanceNodeStartTime} | |||
| instanceNodeStatus={instanceNodeStatus} | |||
| ></LogList> | |||
| ), | |||
| icon: <ProfileOutlined />, | |||
| }, | |||
| { | |||
| key: '2', | |||
| label: '配置参数', | |||
| icon: <DatabaseOutlined />, | |||
| children: <ExperimentParameter nodeData={instanceNodeData} />, | |||
| }, | |||
| { | |||
| key: '3', | |||
| label: '输出结果', | |||
| children: ( | |||
| <ExperimentResult | |||
| experimentInsId={instanceId} | |||
| pipelineNodeId={instanceNodeData.id} | |||
| ></ExperimentResult> | |||
| ), | |||
| icon: <ProfileOutlined />, | |||
| }, | |||
| ], | |||
| [ | |||
| instanceNodeData, | |||
| instanceId, | |||
| instanceName, | |||
| instanceNamespace, | |||
| instanceNodeStatus, | |||
| workflowId, | |||
| instanceNodeStartTime, | |||
| ], | |||
| ); | |||
| return ( | |||
| <Drawer | |||
| title="任务执行详情" | |||
| placement="right" | |||
| getContainer={false} | |||
| closeIcon={false} | |||
| onClose={onClose} | |||
| open={open} | |||
| width={520} | |||
| className={styles['experiment-drawer']} | |||
| destroyOnClose={true} | |||
| > | |||
| <div style={{ paddingTop: '15px' }}> | |||
| <div className={styles['experiment-drawer__info']}> | |||
| 任务名称:{instanceNodeData.label} | |||
| </div> | |||
| <div className={styles['experiment-drawer__info']}> | |||
| 执行状态: | |||
| {instanceNodeStatus ? ( | |||
| <> | |||
| <div | |||
| className={styles['experiment-drawer__status-dot']} | |||
| style={{ | |||
| backgroundColor: experimentStatusInfo[instanceNodeStatus]?.color, | |||
| }} | |||
| ></div> | |||
| <span style={{ color: experimentStatusInfo[instanceNodeStatus]?.color }}> | |||
| {experimentStatusInfo[instanceNodeStatus]?.label} | |||
| </span> | |||
| </> | |||
| ) : ( | |||
| '--' | |||
| )} | |||
| </div> | |||
| <div className={styles['experiment-drawer__info']}> | |||
| 启动时间:{formatDate(instanceNodeStartTime)} | |||
| </div> | |||
| <div className={styles['experiment-drawer__info']}> | |||
| 耗时: | |||
| {elapsedTime(instanceNodeStartTime, instanceNodeEndTime)} | |||
| </div> | |||
| </div> | |||
| <Tabs defaultActiveKey="1" items={items} className={styles['experiment-drawer__tabs']} /> | |||
| </Drawer> | |||
| ); | |||
| }, | |||
| ); | |||
| export default ExperimentDrawer; | |||
| @@ -0,0 +1,131 @@ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { PipelineNodeModelSerialize } from '@/types'; | |||
| import { elapsedTime, formatDate } from '@/utils/date'; | |||
| import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; | |||
| import { Drawer, Tabs } from 'antd'; | |||
| import { useMemo } from 'react'; | |||
| import ExperimentParameter from '../ExperimentParameter'; | |||
| import ExperimentResult from '../ExperimentResult'; | |||
| import LogList from '../LogList'; | |||
| import styles from './index.less'; | |||
| type ExperimentDrawerProps = { | |||
| open: boolean; | |||
| onClose: () => void; | |||
| instanceId: number; // 实验实例 id | |||
| instanceName: string; // 实验实例 name | |||
| instanceNamespace: string; // 实验实例 namespace | |||
| instanceNodeData: PipelineNodeModelSerialize; // 节点数据,在定时刷新实验实例状态中不会变化 | |||
| workflowId?: string; // 实验实例工作流 id | |||
| instanceNodeStatus?: ExperimentStatus; // 实例节点状态 | |||
| instanceNodeStartTime?: string; // 开始时间 | |||
| instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化 | |||
| }; | |||
| const ExperimentDrawer = ({ | |||
| open, | |||
| onClose, | |||
| instanceId, | |||
| instanceName, | |||
| instanceNamespace, | |||
| instanceNodeData, | |||
| workflowId, | |||
| instanceNodeStatus, | |||
| instanceNodeStartTime, | |||
| instanceNodeEndTime, | |||
| }: ExperimentDrawerProps) => { | |||
| // 如果性能有问题,可以进一步拆解 | |||
| const items = useMemo( | |||
| () => [ | |||
| { | |||
| key: '1', | |||
| label: '日志详情', | |||
| children: ( | |||
| <LogList | |||
| instanceName={instanceName} | |||
| instanceNamespace={instanceNamespace} | |||
| pipelineNodeId={instanceNodeData.id} | |||
| workflowId={workflowId} | |||
| instanceNodeStartTime={instanceNodeStartTime} | |||
| instanceNodeStatus={instanceNodeStatus} | |||
| ></LogList> | |||
| ), | |||
| icon: <ProfileOutlined />, | |||
| }, | |||
| { | |||
| key: '2', | |||
| label: '配置参数', | |||
| icon: <DatabaseOutlined />, | |||
| children: <ExperimentParameter nodeData={instanceNodeData} />, | |||
| }, | |||
| { | |||
| key: '3', | |||
| label: '输出结果', | |||
| children: ( | |||
| <ExperimentResult | |||
| experimentInsId={instanceId} | |||
| pipelineNodeId={instanceNodeData.id} | |||
| ></ExperimentResult> | |||
| ), | |||
| icon: <ProfileOutlined />, | |||
| }, | |||
| ], | |||
| [ | |||
| instanceNodeData, | |||
| instanceId, | |||
| instanceName, | |||
| instanceNamespace, | |||
| instanceNodeStatus, | |||
| workflowId, | |||
| instanceNodeStartTime, | |||
| ], | |||
| ); | |||
| return ( | |||
| <Drawer | |||
| title="任务执行详情" | |||
| placement="right" | |||
| getContainer={false} | |||
| closeIcon={false} | |||
| onClose={onClose} | |||
| open={open} | |||
| width={520} | |||
| className={styles['experiment-drawer']} | |||
| destroyOnClose={true} | |||
| mask={false} | |||
| > | |||
| <div style={{ paddingTop: '15px' }}> | |||
| <div className={styles['experiment-drawer__info']}>任务名称:{instanceNodeData.label}</div> | |||
| <div className={styles['experiment-drawer__info']}> | |||
| 执行状态: | |||
| {instanceNodeStatus ? ( | |||
| <> | |||
| <div | |||
| className={styles['experiment-drawer__status-dot']} | |||
| style={{ | |||
| backgroundColor: experimentStatusInfo[instanceNodeStatus]?.color, | |||
| }} | |||
| ></div> | |||
| <span style={{ color: experimentStatusInfo[instanceNodeStatus]?.color }}> | |||
| {experimentStatusInfo[instanceNodeStatus]?.label} | |||
| </span> | |||
| </> | |||
| ) : ( | |||
| '--' | |||
| )} | |||
| </div> | |||
| <div className={styles['experiment-drawer__info']}> | |||
| 启动时间:{formatDate(instanceNodeStartTime)} | |||
| </div> | |||
| <div className={styles['experiment-drawer__info']}> | |||
| 耗时: | |||
| {elapsedTime(instanceNodeStartTime, instanceNodeEndTime)} | |||
| </div> | |||
| </div> | |||
| <Tabs defaultActiveKey="1" items={items} className={styles['experiment-drawer__tabs']} /> | |||
| </Drawer> | |||
| ); | |||
| }; | |||
| export default ExperimentDrawer; | |||
| @@ -8,8 +8,8 @@ import ExportModelModal from '../ExportModelModal'; | |||
| import styles from './index.less'; | |||
| type ExperimentResultProps = { | |||
| experimentInsId?: number; // 实验实例 id | |||
| pipelineNodeId?: string; // 流水线节点 id | |||
| experimentInsId: number; // 实验实例 id | |||
| pipelineNodeId: string; // 流水线节点 id | |||
| }; | |||
| type ExperimentResultData = { | |||
| @@ -6,12 +6,13 @@ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { useStateRef } from '@/hooks'; | |||
| import { ExperimentLog } from '@/pages/Experiment/Info/props'; | |||
| import { getExperimentPodsLog } from '@/services/experiment/index.js'; | |||
| import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons'; | |||
| import { Button } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import dayjs from 'dayjs'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| import { ExperimentLog } from '../LogList'; | |||
| import styles from './index.less'; | |||
| export type LogGroupProps = ExperimentLog & { | |||
| @@ -21,6 +22,7 @@ export type LogGroupProps = ExperimentLog & { | |||
| type Log = { | |||
| start_time: string; // 日志开始时间 | |||
| log_content: string; // 日志内容 | |||
| pod_name: string; // pod名称 | |||
| }; | |||
| // 滚动到底部 | |||
| @@ -49,44 +51,33 @@ function LogGroup({ | |||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||
| const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | |||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | |||
| const socketRef = useRef<WebSocket | undefined>(undefined); | |||
| const retryRef = useRef(2); | |||
| useEffect(() => { | |||
| scrollToBottom(false); | |||
| let timerId: NodeJS.Timeout | undefined; | |||
| if (status === ExperimentStatus.Running) { | |||
| timerId = setInterval(() => { | |||
| requestExperimentPodsLog(); | |||
| }, 5 * 1000); | |||
| setupSockect(); | |||
| } else if (preStatusRef.current === ExperimentStatus.Running) { | |||
| requestExperimentPodsLog(); | |||
| setTimeout(() => { | |||
| requestExperimentPodsLog(); | |||
| }, 5 * 1000); | |||
| setCompleted(true); | |||
| } | |||
| preStatusRef.current = status; | |||
| return () => { | |||
| if (timerId) { | |||
| clearInterval(timerId); | |||
| timerId = undefined; | |||
| } | |||
| }; | |||
| }, [status]); | |||
| // 鼠标拖到中不滚动到底部 | |||
| useEffect(() => { | |||
| const mouseDown = () => { | |||
| setIsMouseDown(true); | |||
| }; | |||
| const mouseUp = () => { | |||
| setIsMouseDown(false); | |||
| }; | |||
| document.addEventListener('mousedown', mouseDown); | |||
| document.addEventListener('mouseup', mouseUp); | |||
| return () => { | |||
| document.removeEventListener('mousedown', mouseDown); | |||
| document.removeEventListener('mouseup', mouseUp); | |||
| closeSocket(); | |||
| }; | |||
| }, []); | |||
| @@ -140,6 +131,84 @@ function LogGroup({ | |||
| requestExperimentPodsLog(); | |||
| }; | |||
| // 建立 socket 连接 | |||
| const setupSockect = () => { | |||
| let { host } = location; | |||
| if (process.env.NODE_ENV === 'development') { | |||
| host = '172.20.32.181:31213'; | |||
| } | |||
| const socket = new WebSocket( | |||
| `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, | |||
| ); | |||
| socket.addEventListener('open', () => { | |||
| // console.log('WebSocket is open now.'); | |||
| }); | |||
| socket.addEventListener('close', (event) => { | |||
| // console.log('WebSocket is closed:', event); | |||
| if (event.code !== 1000 && retryRef.current > 0) { | |||
| retryRef.current -= 1; | |||
| setTimeout(() => { | |||
| setupSockect(); | |||
| }, 2 * 1000); | |||
| } | |||
| }); | |||
| socket.addEventListener('error', (event) => { | |||
| console.error('WebSocket error observed:', event); | |||
| }); | |||
| socket.addEventListener('message', (event) => { | |||
| if (!event.data) { | |||
| return; | |||
| } | |||
| try { | |||
| const data = JSON.parse(event.data); | |||
| const streams = data.streams; | |||
| if (!streams || !Array.isArray(streams)) { | |||
| return; | |||
| } | |||
| let startTime = start_time; | |||
| const logContent = streams.reduce((result, item) => { | |||
| const values = item.values; | |||
| return ( | |||
| result + | |||
| values.reduce((prev: string, cur: [string, string]) => { | |||
| const [time, value] = cur; | |||
| startTime = time; | |||
| const str = `[${dayjs(Number(time) / 1.0e6).format('YYYY-MM-DD HH:mm:ss')}] ${value}`; | |||
| return prev + str; | |||
| }, '') | |||
| ); | |||
| }, ''); | |||
| const logDetail: Log = { | |||
| start_time: startTime!, | |||
| log_content: logContent, | |||
| pod_name: pod_name, | |||
| }; | |||
| setLogList((oldList) => oldList.concat(logDetail)); | |||
| if (!isMouseDownRef.current && logContent) { | |||
| setTimeout(() => { | |||
| scrollToBottom(); | |||
| }, 100); | |||
| } | |||
| } catch (error) { | |||
| console.error('JSON parse error: ', error); | |||
| } | |||
| }); | |||
| socketRef.current = socket; | |||
| }; | |||
| const closeSocket = () => { | |||
| if (socketRef.current) { | |||
| socketRef.current.close(1000, 'completed'); | |||
| socketRef.current = undefined; | |||
| } | |||
| }; | |||
| const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; | |||
| const logText = log_content + logList.map((v) => v.log_content).join(''); | |||
| const showMoreBtn = | |||
| @@ -1,16 +1,22 @@ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { ExperimentLog } from '@/pages/Experiment/Info/props'; | |||
| import { getQueryByExperimentLog } from '@/services/experiment/index.js'; | |||
| import { to } from '@/utils/promise'; | |||
| import dayjs from 'dayjs'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| import LogGroup from '../LogGroup'; | |||
| import styles from './index.less'; | |||
| export type ExperimentLog = { | |||
| log_type: 'normal' | 'resource'; // 日志类型 | |||
| pod_name?: string; // 分布式名称 | |||
| log_content?: string; // 日志内容 | |||
| start_time?: string; // 日志开始时间 | |||
| }; | |||
| type LogListProps = { | |||
| instanceName?: string; // 实验实例 name | |||
| instanceNamespace?: string; // 实验实例 namespace | |||
| pipelineNodeId?: string; // 流水线节点 id | |||
| instanceName: string; // 实验实例 name | |||
| instanceNamespace: string; // 实验实例 namespace | |||
| pipelineNodeId: string; // 流水线节点 id | |||
| workflowId?: string; // 实验实例工作流 id | |||
| instanceNodeStartTime?: string; // 实验实例节点开始运行时间 | |||
| instanceNodeStatus?: ExperimentStatus; | |||
| @@ -25,23 +31,30 @@ function LogList({ | |||
| instanceNodeStatus, | |||
| }: LogListProps) { | |||
| const [logList, setLogList] = useState<ExperimentLog[]>([]); | |||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | |||
| const retryRef = useRef(3); | |||
| useEffect(() => { | |||
| if (workflowId) { | |||
| const start_time = dayjs(instanceNodeStartTime).valueOf() * 1.0e6; | |||
| const params = { | |||
| task_id: pipelineNodeId, | |||
| component_id: workflowId, | |||
| name: instanceName, | |||
| namespace: instanceNamespace, | |||
| start_time: start_time, | |||
| }; | |||
| getExperimentLog(params, start_time); | |||
| if ( | |||
| instanceNodeStatus && | |||
| instanceNodeStatus !== ExperimentStatus.Pending && | |||
| (!preStatusRef.current || preStatusRef.current === ExperimentStatus.Pending) | |||
| ) { | |||
| getExperimentLog(); | |||
| } | |||
| }, [workflowId, instanceNodeStartTime]); | |||
| preStatusRef.current = instanceNodeStatus; | |||
| }, [instanceNodeStatus]); | |||
| // 获取实验日志 | |||
| const getExperimentLog = async (params: any, start_time: number) => { | |||
| const getExperimentLog = async () => { | |||
| const start_time = dayjs(instanceNodeStartTime).valueOf() * 1.0e6; | |||
| const params = { | |||
| task_id: pipelineNodeId, | |||
| component_id: workflowId, | |||
| name: instanceName, | |||
| namespace: instanceNamespace, | |||
| start_time: start_time, | |||
| }; | |||
| const [res] = await to(getQueryByExperimentLog(params)); | |||
| if (res && res.data) { | |||
| const { log_type, pods, log_detail } = res.data; | |||
| @@ -62,6 +75,13 @@ function LogList({ | |||
| })); | |||
| setLogList(list); | |||
| } | |||
| } else { | |||
| if (retryRef.current > 0) { | |||
| retryRef.current -= 1; | |||
| setTimeout(() => { | |||
| getExperimentLog(); | |||
| }, 2 * 1000); | |||
| } | |||
| } | |||
| }; | |||
| @@ -127,7 +127,7 @@ function Experiment() { | |||
| } | |||
| }); | |||
| } catch (error) { | |||
| console.log(error); | |||
| console.error('JSON parse error: ', error); | |||
| } | |||
| } | |||
| }; | |||
| @@ -18,7 +18,7 @@ import { findAllParentNodes } from './utils'; | |||
| let graph = null; | |||
| const EditPipeline = () => { | |||
| const navgite = useNavigate(); | |||
| const navigate = useNavigate(); | |||
| const locationParams = useParams(); //新版本获取路由参数接口 | |||
| const graphRef = useRef(); | |||
| const paramsDrawerRef = useRef(); | |||
| @@ -103,10 +103,8 @@ const EditPipeline = () => { | |||
| setTimeout(() => { | |||
| const data = graph.save(); | |||
| console.log(data); | |||
| const errorNode = data.nodes.find((item) => { | |||
| return item.formError === true; | |||
| }); | |||
| // console.log(data); | |||
| const errorNode = data.nodes.find((item) => item.formError === true); | |||
| if (errorNode) { | |||
| message.error(`【${errorNode.label}】节点必填项必须配置`); | |||
| const graphNode = graph.findById(errorNode.id); | |||
| @@ -124,7 +122,7 @@ const EditPipeline = () => { | |||
| message.success('保存成功'); | |||
| setTimeout(() => { | |||
| if (val) { | |||
| navgite({ pathname: `/pipeline/template` }); | |||
| navigate({ pathname: `/pipeline/template` }); | |||
| } | |||
| }, 500); | |||
| }); | |||
| @@ -46,7 +46,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| const control_strategy = JSON.stringify(fields.control_strategy); | |||
| const in_parameters = JSON.stringify(fields.in_parameters); | |||
| const out_parameters = JSON.stringify(fields.out_parameters); | |||
| console.log('getFieldsValue', fields); | |||
| // console.log('getFieldsValue', fields); | |||
| const res = { | |||
| ...stagingItem, | |||
| @@ -57,7 +57,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| formError: !!error, | |||
| }; | |||
| console.log('res', res); | |||
| // console.log('res', res); | |||
| onFormChange(res); | |||
| } | |||
| }; | |||
| @@ -79,7 +79,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| out_parameters: JSON.parse(model.out_parameters), | |||
| control_strategy: JSON.parse(model.control_strategy), | |||
| }; | |||
| console.log('model', nodeData); | |||
| // console.log('model', nodeData); | |||
| setStagingItem({ | |||
| ...nodeData, | |||
| }); | |||
| @@ -91,7 +91,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| form.validateFields(); | |||
| } | |||
| } catch (error) { | |||
| console.log(error); | |||
| console.error('JSON.parse error: ', error); | |||
| } | |||
| setOpen(true); | |||
| @@ -71,9 +71,6 @@ const Pipeline = () => { | |||
| }); | |||
| } | |||
| }; | |||
| const onFinishFailed = (errorInfo) => { | |||
| console.log('Failed:', errorInfo); | |||
| }; | |||
| const pageOption = useRef({ page: 1, size: 10 }); | |||
| const paginationProps = { | |||
| showSizeChanger: true, | |||
| @@ -86,7 +83,7 @@ const Pipeline = () => { | |||
| }; | |||
| // 当前页面切换 | |||
| const paginationChange = async (current, size) => { | |||
| console.log('page', current, size); | |||
| // console.log('page', current, size); | |||
| pageOption.current = { | |||
| page: current, | |||
| size: size, | |||
| @@ -176,7 +173,6 @@ const Pipeline = () => { | |||
| okText: '确认', | |||
| cancelText: '取消', | |||
| onOk: () => { | |||
| console.log(record); | |||
| cloneWorkflow(record.id).then((ret) => { | |||
| if (ret.code === 200) { | |||
| message.success('复制成功'); | |||
| @@ -214,7 +210,6 @@ const Pipeline = () => { | |||
| title: '删除后,该流水线将不可恢复', | |||
| content: '是否确认删除?', | |||
| onOk: () => { | |||
| console.log(record); | |||
| removeWorkflow(record.id).then((ret) => { | |||
| if (ret.code === 200) { | |||
| message.success('删除成功'); | |||
| @@ -276,7 +271,6 @@ const Pipeline = () => { | |||
| remember: true, | |||
| }} | |||
| onFinish={onFinish} | |||
| onFinishFailed={onFinishFailed} | |||
| autoComplete="off" | |||
| > | |||
| <Form.Item | |||
| @@ -21,7 +21,7 @@ const OperlogDetailForm: React.FC<OperlogFormProps> = (props) => { | |||
| const intl = useIntl(); | |||
| const handleOk = () => { | |||
| console.log('handle ok'); | |||
| // console.log('handle ok'); | |||
| }; | |||
| const handleCancel = () => { | |||
| props.onCancel(); | |||
| @@ -216,7 +216,7 @@ const DataScopeForm: React.FC<DataScopeFormProps> = (props) => { | |||
| checkedKeys={deptIds} | |||
| defaultCheckedKeys={deptCheckedKeys} | |||
| onCheck={(checkedKeys: any, checkInfo: any) => { | |||
| console.log(checkedKeys, checkInfo); | |||
| // console.log(checkedKeys, checkInfo); | |||
| if (checkStrictly) { | |||
| return setDeptIds(checkedKeys.checked); | |||
| } else { | |||
| @@ -210,12 +210,10 @@ const Login: React.FC = () => { | |||
| setSessionToken(response.data?.access_token, response.data?.access_token, expireTime); | |||
| message.success(defaultLoginSuccessMessage); | |||
| await fetchUserInfo(); | |||
| console.log('login ok'); | |||
| const urlParams = new URL(window.location.href).searchParams; | |||
| history.push(urlParams.get('redirect') || '/'); | |||
| return; | |||
| } else { | |||
| console.log(response.msg); | |||
| clearSessionToken(); | |||
| // 如果失败去设置用户错误信息 | |||
| setUserLoginState({ ...response, type }); | |||
| @@ -3,14 +3,17 @@ | |||
| * @Date: 2024-03-25 13:52:54 | |||
| * @Description: 网络请求配置,详情请参考 https://umijs.org/docs/max/request | |||
| */ | |||
| import type { AxiosRequestConfig, AxiosResponse, RequestConfig } from '@umijs/max'; | |||
| import type { AxiosRequestConfig, AxiosResponse, RequestConfig, RequestOptions } from '@umijs/max'; | |||
| import { message } from 'antd'; | |||
| import { clearSessionToken, getAccessToken } from './access'; | |||
| import { setRemoteMenu } from './services/session'; | |||
| import { gotoLoginPage } from './utils/ui'; | |||
| // [antd: Notification] You are calling notice in render which will break in React 18 concurrent mode. Please trigger in effect instead. | |||
| const popupError = (error: string) => { | |||
| const popupError = (error: string, skipErrorHandler?: boolean = false) => { | |||
| if (skipErrorHandler) { | |||
| return; | |||
| } | |||
| // 直接调用 message.error 有时候不弹出来 | |||
| setTimeout(() => { | |||
| message.error(error); | |||
| @@ -39,8 +42,8 @@ export const requestConfig: RequestConfig = { | |||
| responseInterceptors: [ | |||
| [ | |||
| (response: AxiosResponse) => { | |||
| const { status, data } = response || {}; | |||
| // console.log(message, data); | |||
| const { status, data, config } = response || {}; | |||
| const skipErrorHandler = (config as RequestOptions)?.skipErrorHandler; | |||
| if (status >= 200 && status < 300) { | |||
| if (data && (data instanceof Blob || data.code === 200)) { | |||
| return response; | |||
| @@ -51,11 +54,11 @@ export const requestConfig: RequestConfig = { | |||
| popupError('请重新登录'); | |||
| return Promise.reject(response); | |||
| } else { | |||
| popupError(data?.msg ?? '请求失败'); | |||
| popupError(data?.msg ?? '请求失败', skipErrorHandler); | |||
| return Promise.reject(response); | |||
| } | |||
| } else { | |||
| popupError('请求失败'); | |||
| popupError('请求失败', skipErrorHandler); | |||
| return Promise.reject(response); | |||
| } | |||
| }, | |||
| @@ -51,6 +51,7 @@ export function getQueryByExperimentLog(data) { | |||
| return request('/api/mmp/experimentIns/realTimeLog/', { | |||
| method: 'POST', | |||
| data, | |||
| skipErrorHandler: true, | |||
| }); | |||
| } | |||
| // 查询实例节点结果 | |||
| @@ -148,8 +148,7 @@ export function getMatchMenuItem( | |||
| } else { | |||
| const paths = path.split('/'); | |||
| if (paths.length >= 2 && paths[0] === item.path && paths[1] === 'index') { | |||
| console.log(item); | |||
| // console.log(item); | |||
| items.push(item); | |||
| } | |||
| } | |||