Browse Source

feat: 定时更新实验实例节点状态

pull/99/head
cp3hnu 1 year ago
parent
commit
ff0be129a0
16 changed files with 474 additions and 244 deletions
  1. BIN
      react-ui/src/assets/img/experiment-pending.png
  2. BIN
      react-ui/src/assets/img/experiment-running.png
  3. +6
    -1
      react-ui/src/hooks/index.ts
  4. +197
    -60
      react-ui/src/pages/Experiment/Info/index.jsx
  5. +6
    -0
      react-ui/src/pages/Experiment/Info/index.less
  6. +6
    -1
      react-ui/src/pages/Experiment/Info/props.less
  7. +124
    -150
      react-ui/src/pages/Experiment/Info/props.tsx
  8. +2
    -0
      react-ui/src/pages/Experiment/components/ExperimentParameter/index.less
  9. +3
    -4
      react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx
  10. +2
    -0
      react-ui/src/pages/Experiment/components/ExperimentResult/index.less
  11. +22
    -5
      react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx
  12. +23
    -11
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  13. +2
    -0
      react-ui/src/pages/Experiment/components/LogList/index.less
  14. +62
    -6
      react-ui/src/pages/Experiment/components/LogList/index.tsx
  15. +18
    -6
      react-ui/src/pages/Experiment/index.jsx
  16. +1
    -0
      react-ui/src/types.ts

BIN
react-ui/src/assets/img/experiment-pending.png View File

Before After
Width: 66  |  Height: 66  |  Size: 1.6 kB

BIN
react-ui/src/assets/img/experiment-running.png View File

Before After
Width: 66  |  Height: 66  |  Size: 2.1 kB Width: 60  |  Height: 60  |  Size: 2.1 kB

+ 6
- 1
react-ui/src/hooks/index.ts View File

@@ -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;


+ 197
- 60
react-ui/src/pages/Experiment/Info/index.jsx View File

@@ -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>
); );


+ 6
- 0
react-ui/src/pages/Experiment/Info/index.less View File

@@ -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;
}
}
} }

+ 6
- 1
react-ui/src/pages/Experiment/Info/props.less View File

@@ -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%;
}
}
} }
} }
} }


+ 124
- 150
react-ui/src/pages/Experiment/Info/props.tsx View File

@@ -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;

+ 2
- 0
react-ui/src/pages/Experiment/components/ExperimentParameter/index.less View File

@@ -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
- 4
react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx View File

@@ -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,
}} }}


+ 2
- 0
react-ui/src/pages/Experiment/components/ExperimentResult/index.less View File

@@ -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;




+ 22
- 5
react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx View File

@@ -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


+ 23
- 11
react-ui/src/pages/Experiment/components/LogGroup/index.tsx View File

@@ -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' && (


+ 2
- 0
react-ui/src/pages/Experiment/components/LogList/index.less View File

@@ -1,5 +1,7 @@
.log-list { .log-list {
height: 100%;
padding: 8px; padding: 8px;
overflow-y: auto;


&__empty { &__empty {
padding: 15px; padding: 15px;


+ 62
- 6
react-ui/src/pages/Experiment/components/LogList/index.tsx View File

@@ -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>
)} )}


+ 18
- 6
react-ui/src/pages/Experiment/index.jsx View File

@@ -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%',
}, },
{ {


+ 1
- 0
react-ui/src/types.ts View File

@@ -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;
}; };


// 流水线节点模型数据 // 流水线节点模型数据


Loading…
Cancel
Save