Browse Source

feat: 实验实例分页

pull/99/head
cp3hnu 1 year ago
parent
commit
5317888b3a
17 changed files with 474 additions and 320 deletions
  1. +4
    -0
      react-ui/src/components/ParameterInput/index.less
  2. +7
    -1
      react-ui/src/components/ParameterInput/index.tsx
  3. +1
    -1
      react-ui/src/components/ResourceSelect/index.less
  4. +3
    -3
      react-ui/src/components/ResourceSelect/index.tsx
  5. +69
    -0
      react-ui/src/pages/Experiment/components/ExperimentInstance/index.less
  6. +178
    -0
      react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx
  7. +1
    -1
      react-ui/src/pages/Experiment/components/TensorBoardStatus/index.tsx
  8. +108
    -183
      react-ui/src/pages/Experiment/index.jsx
  9. +12
    -86
      react-ui/src/pages/Experiment/index.less
  10. +7
    -1
      react-ui/src/pages/Mirror/Info/index.tsx
  11. +2
    -1
      react-ui/src/pages/Mirror/List/index.tsx
  12. +1
    -0
      react-ui/src/pages/ModelDeployment/List/index.tsx
  13. +27
    -12
      react-ui/src/pages/Pipeline/editPipeline/index.jsx
  14. +43
    -25
      react-ui/src/pages/Pipeline/editPipeline/props.tsx
  15. +3
    -2
      react-ui/src/services/experiment/index.js
  16. +6
    -2
      react-ui/src/types.ts
  17. +2
    -2
      react-ui/src/utils/promise.ts

+ 4
- 0
react-ui/src/components/ParameterInput/index.less View File

@@ -62,3 +62,7 @@
font-size: 12px;
}
}

.parameter-input.parameter-input--error {
border-color: @error-color;
}

+ 7
- 1
react-ui/src/components/ParameterInput/index.tsx View File

@@ -1,5 +1,5 @@
import { CloseOutlined } from '@ant-design/icons';
import { Input } from 'antd';
import { Form, Input } from 'antd';
import { RuleObject } from 'antd/es/form';
import classNames from 'classnames';
import './index.less';
@@ -31,6 +31,7 @@ export interface ParameterInputProps {
style?: React.CSSProperties;
size?: 'middle' | 'small' | 'large';
disabled?: boolean;
id?: string;
}

function ParameterInput({
@@ -45,6 +46,7 @@ function ParameterInput({
style,
size = 'middle',
disabled = false,
id,
...rest
}: ParameterInputProps) {
const valueObj =
@@ -55,6 +57,7 @@ function ParameterInput({
const isSelect = valueObj?.fromSelect;
const placeholder = valueObj?.placeholder || rest?.placeholder;
const InputComponent = textArea ? Input.TextArea : Input;
const { status } = Form.Item.useStatus();

// 删除
const handleRemove = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
@@ -75,9 +78,11 @@ function ParameterInput({
<>
{(isSelect || !canInput) && !disabled ? (
<div
id={id}
className={classNames(
'parameter-input',
{ 'parameter-input--large': size === 'large' },
{ [`parameter-input--${status}`]: status },
className,
)}
style={style}
@@ -98,6 +103,7 @@ function ParameterInput({
) : (
<InputComponent
{...rest}
id={id}
size={size}
className={className}
style={style}


+ 1
- 1
react-ui/src/components/ResourceSelect/index.less View File

@@ -1,4 +1,4 @@
.resource-select {
.kf-resource-select {
position: relative;
display: flex;
align-items: center;


+ 3
- 3
react-ui/src/components/ResourceSelect/index.tsx View File

@@ -8,7 +8,7 @@ import { openAntdModal } from '@/utils/modal';
import { Button } from 'antd';
import { useState } from 'react';
import ParameterInput, { type ParameterInputProps } from '../ParameterInput';
import styles from './index.less';
import './index.less';

export { requiredValidator, type ParameterInputObject } from '../ParameterInput';

@@ -80,7 +80,7 @@ function ResourceSelect({ type, value, onChange, ...rest }: ResourceSelectProps)
};

return (
<div className={styles['resource-select']}>
<div className="kf-resource-select">
<ParameterInput
{...rest}
value={value}
@@ -89,7 +89,7 @@ function ResourceSelect({ type, value, onChange, ...rest }: ResourceSelectProps)
onClick={selectResource}
></ParameterInput>
<Button
className={styles['resource-select__button']}
className="kf-resource-select__button"
size="large"
type="link"
icon={getSelectBtnIcon(type)}


+ 69
- 0
react-ui/src/pages/Experiment/components/ExperimentInstance/index.less View File

@@ -0,0 +1,69 @@
.tableExpandBox {
display: flex;
align-items: center;
width: 100%;
padding: 0 0 0 33px;
color: @text-color;
font-size: 15px;

& > div {
padding: 0 16px;
}

.index {
width: calc((100% + 32px + 33px) / 6.25);
}

.tensorBoard {
width: calc((100% + 32px + 33px) / 6.25);
}

.description {
display: flex;
flex: 1;
align-items: center;

.startTime {
.singleLine();
}
}

.status {
width: 200px;
}

.operation {
width: 334px;
}
}

.tableExpandBoxContent {
height: 45px;
background-color: #fff;
border: 1px solid #eaeaea;

& + & {
border-top: none;
}

.statusBox {
display: flex;
align-items: center;
width: 200px;

.statusIcon {
visibility: hidden;
transition: all 0.2s;
}
}
.statusBox:hover .statusIcon {
visibility: visible;
}
}

.loadMoreBox {
display: flex;
align-items: center;
justify-content: center;
margin: 10px auto 0;
}

+ 178
- 0
react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx View File

@@ -0,0 +1,178 @@
import KFIcon from '@/components/KFIcon';
import { ExperimentStatus } from '@/enums';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import {
deleteQueryByExperimentInsId,
putQueryByExperimentInsId,
} from '@/services/experiment/index.js';
import themes from '@/styles/theme.less';
import { type ExperimentInstance } from '@/types';
import { elapsedTime, formatDate } from '@/utils/date';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { DoubleRightOutlined } from '@ant-design/icons';
import { App, Button, ConfigProvider, Tooltip } from 'antd';
import classNames from 'classnames';
import TensorBoardStatusCell from '../TensorBoardStatus';
import styles from './index.less';

type ExperimentInstanceProps = {
experimentInList?: ExperimentInstance[];
experimentInsTotal: number;
onClickInstance?: (instance: ExperimentInstance) => void;
onClickTensorBoard?: (instance: ExperimentInstance) => void;
onRemove?: () => void;
onTerminate?: (instance: ExperimentInstance) => void;
onLoadMore?: () => void;
};

function ExperimentInstanceComponent({
experimentInList,
experimentInsTotal,
onClickInstance,
onClickTensorBoard,
onRemove,
onTerminate,
onLoadMore,
}: ExperimentInstanceProps) {
const { message } = App.useApp();

// 删除实验实例确认
const handleRemove = (instance: ExperimentInstance) => {
modalConfirm({
title: '确定删除该条实例吗?',
onOk: () => {
deleteExperimentInstance(instance.id);
},
});
};

// 删除实验实例
const deleteExperimentInstance = async (id: number) => {
const [res] = await to(deleteQueryByExperimentInsId(id));
if (res) {
message.success('删除成功');
onRemove?.();
}
};

// 终止实验实例
const terminateExperimentInstance = async (instance: ExperimentInstance) => {
const [res] = await to(putQueryByExperimentInsId(instance.id));
if (res) {
message.success('终止成功');
onTerminate?.(instance);
}
};

if (!experimentInList || experimentInList.length === 0) {
return null;
}

return (
<div>
<div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}>
<div className={styles.index}>序号</div>
<div className={styles.tensorBoard}>可视化</div>
<div className={styles.description}>
<div style={{ width: '50%' }}>运行时长</div>
<div style={{ width: '50%' }}>开始时间</div>
</div>
<div className={styles.status}>状态</div>
<div className={styles.operation}>操作</div>
</div>

{experimentInList.map((item, index) => (
<div
key={item.id}
className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)}
>
<a
className={styles.index}
style={{ padding: '0 16px' }}
onClick={() => onClickInstance?.(item)}
>
{index + 1}
</a>
<div className={styles.tensorBoard}>
{item.nodes_result?.tensorboard_log ? (
<TensorBoardStatusCell
status={item.tensorBoardStatus}
onClick={() => onClickTensorBoard?.(item)}
></TensorBoardStatusCell>
) : (
'--'
)}
</div>
<div className={styles.description}>
<div style={{ width: '50%' }}>{elapsedTime(item.create_time, item.finish_time)}</div>
<div style={{ width: '50%' }} className={styles.startTime}>
<Tooltip title={formatDate(item.create_time)}>
<span>{formatDate(item.create_time)}</span>
</Tooltip>
</div>
</div>
<div className={styles.statusBox}>
<img
style={{ width: '17px', marginRight: '7px' }}
src={experimentStatusInfo[item.status as ExperimentStatus]?.icon}
/>
<span
style={{ color: experimentStatusInfo[item.status as ExperimentStatus]?.color }}
className={styles.statusIcon}
>
{experimentStatusInfo[item.status as ExperimentStatus]?.label}
</span>
</div>
<div className={styles.operation}>
<Button
type="link"
size="small"
key="stop"
disabled={
item.status === ExperimentStatus.Succeeded ||
item.status === ExperimentStatus.Failed ||
item.status === ExperimentStatus.Terminated
}
icon={<KFIcon type="icon-zhongzhi" />}
onClick={() => terminateExperimentInstance(item)}
>
终止
</Button>
<ConfigProvider
theme={{
token: {
colorLink: themes['warningColor'],
},
}}
>
<Button
type="link"
size="small"
key="batchRemove"
disabled={
item.status === ExperimentStatus.Running ||
item.status === ExperimentStatus.Pending
}
icon={<KFIcon type="icon-shanchu" />}
onClick={() => handleRemove(item)}
>
删除
</Button>
</ConfigProvider>
</div>
</div>
))}
{experimentInsTotal > experimentInList.length ? (
<div className={styles.loadMoreBox}>
<Button type="link" onClick={onLoadMore}>
更多
<DoubleRightOutlined rotate={90} />
</Button>
</div>
) : null}
</div>
);
}

export default ExperimentInstanceComponent;

+ 1
- 1
react-ui/src/pages/Experiment/components/TensorBoardStatus/index.tsx View File

@@ -42,7 +42,7 @@ const statusConfig: Record<TensorBoardStatus, TensorBoardStatusInfo> = {
};

type TensorBoardStatusProps = {
status: TensorBoardStatus;
status?: TensorBoardStatus;
onClick: () => void;
};



+ 108
- 183
react-ui/src/pages/Experiment/index.jsx View File

@@ -1,31 +1,28 @@
import CommonTableCell from '@/components/CommonTableCell';
import KFIcon from '@/components/KFIcon';
import { TensorBoardStatus } from '@/enums';
import { ExperimentStatus, TensorBoardStatus } from '@/enums';
import {
deleteExperimentById,
deleteQueryByExperimentInsId,
getExperiment,
getExperimentById,
getQueryByExperimentId,
getTensorBoardStatusReq,
postExperiment,
putExperiment,
putQueryByExperimentInsId,
runExperiments,
runTensorBoardReq,
} from '@/services/experiment/index.js';
import { getWorkflow } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less';
import { elapsedTime, formatDate } from '@/utils/date';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { App, Button, ConfigProvider, Dropdown, Space, Table, Tooltip } from 'antd';
import { App, Button, ConfigProvider, Dropdown, Space, Table } from 'antd';
import classNames from 'classnames';
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ComparisonType } from './Comparison/config';
import AddExperimentModal from './components/AddExperimentModal';
import TensorBoardStatusCell from './components/TensorBoardStatus';
import ExperimentInstance from './components/ExperimentInstance';
import Styles from './index.less';
import { experimentStatusInfo } from './status';

@@ -49,7 +46,18 @@ function Experiment() {
const [isAdd, setIsAdd] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [addFormData, setAddFormData] = useState({});
const [experimentInsTotal, setExperimentInsTotal] = useState(0);
const { message } = App.useApp();
const pageOption = useRef({ page: 1, size: 10 });
const paginationProps = {
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
total: total,
page: pageOption.current.page,
size: pageOption.current.size,
onChange: (current, size) => paginationChange(current, size),
};

useEffect(() => {
getList();
@@ -83,38 +91,42 @@ function Experiment() {
setWorkflowList(res.data.content);
}
};
// 获取实验实例
const getQueryByExperiment = (val) => {
getQueryByExperimentId(val).then((ret) => {
setExpandedRowKeys(val);
if (ret && ret.data && ret.data.length > 0) {
try {
const list = ret.data.map((v) => {
const nodes_result = v.nodes_result ? JSON.parse(v.nodes_result) : {};
return {
...v,
nodes_result,
};
});
// 获取实验实例列表
const getQueryByExperiment = async (experimentId, page) => {
const params = {
experimentId: experimentId,
page: page,
size: 5,
};
const [res, error] = await to(getQueryByExperimentId(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
setExpandedRowKeys(experimentId);
try {
const list = content.map((v) => {
const nodes_result = v.nodes_result ? JSON.parse(v.nodes_result) : {};
return {
...v,
nodes_result,
};
});
if (page === 0) {
setExperimentInList(list);
// 获取 TensorBoard 状态
list.forEach((item) => {
if (item.nodes_result?.tensorboard_log) {
const timerId = setTimeout(() => {
getTensorBoardStatus(item);
}, 0);
timerIds.set(item.id, timerId);
}
});
} catch (error) {
setExperimentInList([]);
clearExperimentInTimers();
} else {
setExperimentInList((prev) => [...prev, ...list]);
}
getList();
} else {
setExperimentInList([]);
getList();
setExperimentInsTotal(totalElements);
// 获取 TensorBoard 状态
list.forEach((item) => {
if (item.nodes_result?.tensorboard_log) {
getTensorBoardStatus(item);
}
});
} catch (error) {
console.log(error);
}
});
}
};
// 运行 TensorBoard
const runTensorBoard = async (experimentIn) => {
@@ -155,18 +167,26 @@ function Experiment() {
return item;
});
});
const timerId = setTimeout(() => {

let timerId = timerIds.get(experimentIn.id);
if (timerId) {
clearTimeout(timerId);
timerIds.delete(experimentIn.id);
}
timerId = setTimeout(() => {
getTensorBoardStatus(experimentIn);
}, 10000);
}, 1000 * 1000);
timerIds.set(experimentIn.id, timerId);
}
};
// 展开实例
const expandChange = (e, record) => {
clearExperimentInTimers();
setExperimentInList([]);
if (record.id === expandedRowKeys) {
setExpandedRowKeys(null);
} else {
getQueryByExperiment(record.id);
getQueryByExperiment(record.id, 0);
}
};
// 终止实验实例获取 TensorBoard 状态的定时器
@@ -198,6 +218,7 @@ function Experiment() {
const handleCancel = () => {
setIsModalOpen(false);
};
// 跳转到流水线
const routeToEdit = (e, record) => {
e.stopPropagation();
navgite({ pathname: `/pipeline/template/${record.workflow_id}` });
@@ -226,16 +247,7 @@ function Experiment() {
}
}
};
const pageOption = useRef({ page: 1, size: 10 });
const paginationProps = {
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
total: total,
page: pageOption.current.page,
size: pageOption.current.size,
onChange: (current, size) => paginationChange(current, size),
};

// 当前页面切换
const paginationChange = async (current, size) => {
pageOption.current = {
@@ -244,21 +256,22 @@ function Experiment() {
};
getList();
};
const runExperiment = (id) => {
runExperiments(id).then((ret) => {
if (ret.code === 200) {
message.success('运行成功');
getQueryByExperiment(id);
} else {
message.error('运行失败');
}
});
// 运行实验
const runExperiment = async (id) => {
const [res] = await to(runExperiments(id));
if (res) {
message.success('运行成功');
refreshExperimentIns(id);
} else {
message.error('运行失败');
}
};
const routerToText = (e, item, record) => {
e.stopPropagation();
// 跳转到实验实例详情
const gotoInstanceInfo = (item, record) => {
navgite({ pathname: `/pipeline/experiment/${record.workflow_id}/${item.id}` });
};

// 处理 TensorBoard 操作
const handleTensorboard = async (experimentIn) => {
if (
experimentIn.tensorBoardStatus === TensorBoardStatus.Terminated ||
@@ -273,6 +286,21 @@ function Experiment() {
}
};

// 实验实例终止
const handleInstanceTerminate = async (experimentIn) => {
setExperimentInList((prevList) => {
return prevList.map((item) => {
if (item.id === experimentIn.id) {
return {
...item,
status: ExperimentStatus.Terminated,
};
}
return item;
});
});
};

// 实验对比菜单
const getComparisonMenu = (experimentId) => {
return {
@@ -292,6 +320,17 @@ function Experiment() {
};
};

// 刷新实验实例列表
const refreshExperimentIns = (experimentId) => {
getQueryByExperiment(experimentId, 0);
};

// 加载更多实验实例
const loadMoreExperimentIns = () => {
const page = Math.round(experimentInList.length / 5);
getQueryByExperiment(expandedRowKeys, page);
};

const columns = [
{
title: '实验名称',
@@ -413,7 +452,7 @@ function Experiment() {
];
return (
<div className={Styles.experimentBox}>
<div className={Styles.pipelineTopBox}>
<div className={Styles.experimentTopBox}>
<Button type="default" onClick={createExperiment} icon={<KFIcon type="icon-xinjian2" />}>
新建实验
</Button>
@@ -427,130 +466,16 @@ function Experiment() {
scroll={{ y: 'calc(100% - 55px)' }}
expandable={{
expandedRowRender: (record) => (
<div>
{experimentInList && experimentInList.length > 0 ? (
<div className={Styles.tableExpandBox} style={{ paddingBottom: '16px' }}>
<div className={Styles.index}>序号</div>
<div className={Styles.tensorBoard}>可视化</div>
<div className={Styles.description}>
<div style={{ width: '50%' }}>运行时长</div>
<div style={{ width: '50%' }}>开始时间</div>
</div>
<div className={Styles.status}>状态</div>
<div className={Styles.operation}>操作</div>
</div>
) : (
''
)}

{experimentInList && experimentInList.length > 0
? experimentInList.map((item, index) => (
<div
key={item.id}
className={classNames(Styles.tableExpandBox, Styles.tableExpandBoxContent)}
>
<a
className={Styles.index}
style={{ padding: '0 16px' }}
onClick={(e) => routerToText(e, item, record)}
>
{index + 1}
</a>
<div className={Styles.tensorBoard}>
{item.nodes_result?.tensorboard_log ? (
<TensorBoardStatusCell
status={item.tensorBoardStatus}
onClick={() => handleTensorboard(item)}
></TensorBoardStatusCell>
) : (
'--'
)}
</div>
<div className={Styles.description}>
<div style={{ width: '50%' }}>
{elapsedTime(item.create_time, item.finish_time)}
</div>
<div style={{ width: '50%' }} className={Styles.startTime}>
<Tooltip title={formatDate(item.create_time)}>
<span>{formatDate(item.create_time)}</span>
</Tooltip>
</div>
</div>
<div className={Styles.statusBox}>
<img
style={{ width: '17px', marginRight: '7px' }}
src={experimentStatusInfo[item.status]?.icon}
/>
<span
style={{ color: experimentStatusInfo[item.status]?.color }}
className={Styles.statusIcon}
>
{experimentStatusInfo[item.status]?.label}
</span>
</div>
<div className={Styles.operation}>
<Button
type="link"
size="small"
key="stop"
disabled={
item.status === 'Succeeded' ||
item.status === 'Failed' ||
item.status === 'Terminated'
}
icon={<KFIcon type="icon-zhongzhi" />}
onClick={async () => {
putQueryByExperimentInsId(item.id).then((ret) => {
if (ret.code === 200) {
message.success('终止成功');
getQueryByExperiment(record.id);
} else {
message.error(ret.msg);
}
});
}}
>
终止
</Button>
<ConfigProvider
theme={{
token: {
colorLink: themes['warningColor'],
},
}}
>
<Button
type="link"
size="small"
key="batchRemove"
disabled={item.status === 'Running' || item.status === 'Pending'}
icon={<KFIcon type="icon-shanchu" />}
onClick={() => {
modalConfirm({
title: '确定删除该条实例吗?',
onOk: () => {
deleteQueryByExperimentInsId(item.id).then((ret) => {
if (ret.code === 200) {
message.success('删除成功');
getQueryByExperiment(record.id);
} else {
message.error(ret.msg);
}
});
},
});
}}
>
删除
</Button>
</ConfigProvider>
</div>
</div>
))
: ''}
</div>
<ExperimentInstance
experimentInList={experimentInList}
experimentInsTotal={experimentInsTotal}
onClickInstance={(item) => gotoInstanceInfo(item, record)}
onClickTensorBoard={handleTensorboard}
onRemove={() => refreshExperimentIns(record.id)}
onTerminate={handleInstanceTerminate}
onLoadMore={() => loadMoreExperimentIns()}
></ExperimentInstance>
),

onExpand: (e, a) => {
expandChange(e, a);
},


+ 12
- 86
react-ui/src/pages/Experiment/index.less View File

@@ -1,92 +1,18 @@
.experimentTopBox {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
height: 49px;
padding-right: 30px;
background-image: url(/assets/images/pipeline-back.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
}
.pipelineTopBox {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
height: 49px;
margin-bottom: 10px;
padding-right: 30px;
background-image: url(/assets/images/pipeline-back.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
}
.tableExpandBox {
display: flex;
align-items: center;
width: 100%;
padding: 0 0 0 33px;
color: @text-color;
font-size: 15px;

& > div {
padding: 0 16px;
}

.index {
width: calc((100% + 32px + 33px) / 6.25);
}

.tensorBoard {
width: calc((100% + 32px + 33px) / 6.25);
}

.description {
.experimentBox {
height: 100%;
.experimentTopBox {
display: flex;
flex: 1;
align-items: center;

.startTime {
.singleLine();
}
}

.status {
width: 200px;
justify-content: flex-end;
width: 100%;
height: 49px;
margin-bottom: 10px;
padding-right: 30px;
background-image: url(/assets/images/pipeline-back.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
}

.operation {
width: 334px;
}
}
.tableExpandBoxContent {
height: 45px;
background-color: #fff;
border: 1px solid #eaeaea;

& + & {
border-top: none;
}
}

.statusBox {
display: flex;
align-items: center;
width: 200px;

.statusIcon {
visibility: hidden;
transition: all 0.2s;
}
}
.statusBox:hover .statusIcon {
visibility: visible;
}

.experimentBox {
height: 100%;
.experimentTable {
height: calc(100% - 60px);
:global {


+ 7
- 1
react-ui/src/pages/Mirror/Info/index.tsx View File

@@ -288,7 +288,13 @@ function MirrorInfo() {
dataSource={tableData}
columns={columns}
scroll={{ y: 'calc(100% - 55px)' }}
pagination={{ ...pagination, total, showSizeChanger: true, showQuickJumper: true }}
pagination={{
...pagination,
total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
rowKey="id"
/>


+ 2
- 1
react-ui/src/pages/Mirror/List/index.tsx View File

@@ -241,7 +241,7 @@ function MirrorList() {
<div className={styles['mirror-list__content']}>
<div className={styles['mirror-list__content__filter']}>
<Input.Search
placeholder="按数据集名称筛选"
placeholder="按镜像名称筛选"
allowClear
onSearch={onSearch}
onChange={(e) => setInputText(e.target.value)}
@@ -277,6 +277,7 @@ function MirrorList() {
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
rowKey="id"


+ 1
- 0
react-ui/src/pages/ModelDeployment/List/index.tsx View File

@@ -336,6 +336,7 @@ function ModelDeployment() {
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
rowKey="service_id"


+ 27
- 12
react-ui/src/pages/Pipeline/editPipeline/index.jsx View File

@@ -86,14 +86,21 @@ const EditPipeline = () => {

// 保存
const savePipeline = async (val) => {
const [res, error] = await to(paramsDrawerRef.current.validateFields());
if (error) {
const [globalParamRes, globalParamError] = await to(paramsDrawerRef.current.validateFields());
if (globalParamError) {
message.error('全局参数配置有误');
openParamsDrawer();
return;
}
closeParamsDrawer();

const [propsRes, propsError] = await to(propsRef.current.validateFields());
if (propsError) {
message.error('节点必填项必须配置');
return;
}
propsRef.current.close();

propsRef.current.propClose();
setTimeout(() => {
const data = graph.save();
console.log(data);
@@ -102,16 +109,19 @@ const EditPipeline = () => {
});
if (errorNode) {
message.error(`【${errorNode.label}】节点必填项必须配置`);
const graphNode = graph.findById(errorNode.id);
if (graphNode) {
openNodeDrawer(graphNode, true);
}
return;
}
const params = {
...locationParams,
dag: JSON.stringify(data),
global_param: JSON.stringify(res.global_param),
global_param: JSON.stringify(globalParamRes.global_param),
};
saveWorkflow(params).then((ret) => {
message.success('保存成功');
closeParamsDrawer();
setTimeout(() => {
if (val) {
navgite({ pathname: `/pipeline/template` });
@@ -281,6 +291,17 @@ const EditPipeline = () => {
}
};

// 打开节点抽屉
const openNodeDrawer = (node, validate = false) => {
// 获取所有的上游节点
const parentNodes = findAllParentNodes(graph, node);
// 如果没有打开过全局参数抽屉,获取不到全局参数
const globalParams =
paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current;
// 打开节点编辑抽屉
propsRef.current.showDrawer(node.getModel(), globalParams, parentNodes, validate);
};

// 初始化图
const initGraph = () => {
const contextMenu = initMenu();
@@ -531,13 +552,7 @@ const EditPipeline = () => {
const bindEvents = () => {
graph.on('node:click', (e) => {
if (e.target.get('name') !== 'anchor-point' && e.item) {
// 获取所有的上游节点
const parentNodes = findAllParentNodes(graph, e.item);
// 如果没有打开过全局参数抽屉,获取不到全局参数
const globalParams =
paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current;
// 打开节点编辑抽屉
propsRef.current.showDrawer(e, globalParams, parentNodes);
openNodeDrawer(e.item);
}
});
graph.on('aftercreateedge', (e) => {


+ 43
- 25
react-ui/src/pages/Pipeline/editPipeline/props.tsx View File

@@ -6,6 +6,7 @@ import { CommonTabKeys } from '@/enums';
import { useComputingResource } from '@/hooks/resource';
import {
PipelineGlobalParam,
PipelineNodeModel,
PipelineNodeModelParameter,
PipelineNodeModelSerialize,
} from '@/types';
@@ -57,7 +58,6 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
};

console.log('res', res);

onFormChange(res);
}
};
@@ -66,36 +66,53 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
};

useImperativeHandle(ref, () => ({
showDrawer(e: any, params: PipelineGlobalParam[], parentNodes: INode[]) {
if (e.item && e.item.getModel()) {
showDrawer(
model: PipelineNodeModel,
params: PipelineGlobalParam[],
parentNodes: INode[],
validate: boolean = false,
) {
try {
const nodeData: PipelineNodeModelSerialize = {
...model,
in_parameters: JSON.parse(model.in_parameters),
out_parameters: JSON.parse(model.out_parameters),
control_strategy: JSON.parse(model.control_strategy),
};
console.log('model', nodeData);
setStagingItem({
...nodeData,
});
form.resetFields();
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),
};
console.log('model', nodeData);
setStagingItem({
...nodeData,
});
form.setFieldsValue({
...nodeData,
});
} catch (error) {
console.log(error);
form.setFieldsValue({
...nodeData,
});
if (validate) {
form.validateFields();
}
setOpen(true);

// 参数下拉菜单
setMenuItems(createMenuItems(params, parentNodes));
} catch (error) {
console.log(error);
}
setOpen(true);

// 参数下拉菜单
setMenuItems(createMenuItems(params, parentNodes));
},
propClose: () => {
close: () => {
onClose();
},
validateFields: async () => {
if (!open) {
return;
}
const [values, error] = await to(form.validateFields());
if (!error && values) {
return values;
} else {
form.scrollToField((error as any)?.errorFields?.[0]?.name, { block: 'center' });
return Promise.reject(error);
}
},
}));

// 选择数据集、模型、镜像
@@ -279,6 +296,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
maxWidth: 600,
}}
autoComplete="off"
scrollToFirstError
>
<div className={styles['pipeline-drawer__title']}>
<SubAreaTitle image="/assets/images/static-message.png" title="基本信息"></SubAreaTitle>


+ 3
- 2
react-ui/src/services/experiment/index.js View File

@@ -28,9 +28,10 @@ export function deleteExperimentById(id) {
});
}
// 根据id查询实验实例
export function getQueryByExperimentId(id) {
return request(`/api/mmp/experimentIns/queryByExperimentId/${id}`, {
export function getQueryByExperimentId(params) {
return request(`/api/mmp/experimentIns`, {
method: 'GET',
params,
});
}
// 根据id删除实验实例


+ 6
- 2
react-ui/src/types.ts View File

@@ -4,7 +4,7 @@
* @Description: 定义全局类型,比如无关联的页面都需要要的类型
*/

import { ExperimentStatus } from '@/enums';
import { ExperimentStatus, TensorBoardStatus } from '@/enums';

// 流水线全局参数
export type PipelineGlobalParam = {
@@ -26,9 +26,13 @@ export type ExperimentInstance = {
status: string;
argo_ins_name: string;
argo_ins_ns: string;
nodes_result: string;
nodes_result: {
[key: string]: any;
};
nodes_status: string;
global_param: PipelineGlobalParam[];
tensorBoardStatus?: TensorBoardStatus;
tensorboardUrl?: string;
};

// 流水线节点


+ 2
- 2
react-ui/src/utils/promise.ts View File

@@ -2,12 +2,12 @@
* @param { Promise } promise
* @return { Promise }
*/
export async function to<T>(promise: Promise<T>): Promise<[T, null] | [null, Error]> {
export async function to<T, U = Error>(promise: Promise<T>): Promise<[T, null] | [null, U]> {
try {
const data = await promise;
return [data, null];
} catch (error) {
return [null, error as Error];
return [null, error as U];
}
}



Loading…
Cancel
Save