Browse Source

feat: 实验实例批量删除 & 编辑实验添加【确定并运行】按钮

pull/137/head
cp3hnu 1 year ago
parent
commit
0281084c92
6 changed files with 169 additions and 21 deletions
  1. +43
    -1
      react-ui/src/hooks/index.ts
  2. +1
    -1
      react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less
  3. +28
    -7
      react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx
  4. +6
    -1
      react-ui/src/pages/Experiment/components/ExperimentInstance/index.less
  5. +66
    -2
      react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx
  6. +25
    -9
      react-ui/src/pages/Experiment/index.jsx

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

@@ -5,7 +5,7 @@
*/
import { FormInstance } from 'antd';
import { debounce } from 'lodash';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
/**
* 生成具有初始值的状态引用
*
@@ -156,3 +156,45 @@ export const useEffectWhen = (effect: () => void, deps: React.DependencyList, wh
}
}, [when]);
};

// 选择、全选操作
export const useCheck = <T>(list: T[]) => {
const [selected, setSelected] = useState<T[]>([]);

const checked = useMemo(() => {
return selected.length === list.length;
}, [selected, list]);

const indeterminate = useMemo(() => {
return selected.length > 0 && selected.length < list.length;
}, [selected, list]);

const checkAll = useCallback(() => {
setSelected(checked ? [] : list);
}, [list, checked]);

const isSingleChecked = useCallback((item: T) => selected.includes(item), [selected]);

const checkSingle = useCallback(
(item: T) => {
setSelected((prev) => {
if (isSingleChecked(item)) {
return prev.filter((i) => i !== item);
} else {
return [...prev, item];
}
});
},
[selected, isSingleChecked],
);

return [
selected,
setSelected,
checked,
indeterminate,
checkAll,
isSingleChecked,
checkSingle,
] as const;
};

+ 1
- 1
react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less View File

@@ -39,7 +39,7 @@
}

&__url {
margin-bottom: 10px;
margin-bottom: 10px !important;
color: @text-color-secondary;
font-size: 14px;
}


+ 28
- 7
react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx View File

@@ -2,7 +2,8 @@ import createExperimentIcon from '@/assets/img/create-experiment.png';
import editExperimentIcon from '@/assets/img/edit-experiment.png';
import KFModal from '@/components/KFModal';
import { type PipelineGlobalParam } from '@/types';
import { Form, Input, Radio, Select, type FormRule } from 'antd';
import { to } from '@/utils/promise';
import { Button, Form, Input, Radio, Select, type FormRule } from 'antd';
import { useState } from 'react';
import styles from './index.less';

@@ -17,7 +18,7 @@ type AddExperimentModalProps = {
isAdd: boolean;
open: boolean;
onCancel: () => void;
onFinish: () => void;
onFinish: (values: any, isRun: boolean) => void;
workflowList: Workflow[];
initialValues: FormData;
};
@@ -113,25 +114,45 @@ function AddExperimentModal({
form.setFieldValue('global_param', []);
}
};

const handleRun = async (run: boolean) => {
const [values, error] = await to(form.validateFields());
if (!error && values) {
onFinish(values, run);
}
};

const footer = [
<Button key="cancel" onClick={onCancel}>
取消
</Button>,
<Button key="submit" type="primary" onClick={() => handleRun(false)}>
确定
</Button>,
];
if (!isAdd) {
footer.push(
<Button key="run" type="primary" onClick={() => handleRun(true)}>
确定并运行
</Button>,
);
}

return (
<KFModal
className={styles['add-experiment-modal']}
title={modalTitle}
image={modalIcon}
open={open}
okButtonProps={{
htmlType: 'submit',
form: 'form',
}}
onCancel={onCancel}
destroyOnClose={true}
width={825}
footer={footer}
>
<Form
name="form"
layout="horizontal"
initialValues={initialValues}
onFinish={onFinish}
autoComplete="off"
form={form}
{...layout}


+ 6
- 1
react-ui/src/pages/Experiment/components/ExperimentInstance/index.less View File

@@ -10,8 +10,12 @@
padding: 0 16px;
}

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

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

.tensorBoard {
@@ -33,6 +37,7 @@
}

.operation {
position: relative;
width: 344px;
}
}


+ 66
- 2
react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx View File

@@ -1,7 +1,9 @@
import KFIcon from '@/components/KFIcon';
import { ExperimentStatus } from '@/enums';
import { useCheck } from '@/hooks';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import {
deleteManyExperimentIns,
deleteQueryByExperimentInsId,
putQueryByExperimentInsId,
} from '@/services/experiment/index.js';
@@ -11,8 +13,9 @@ 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 { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd';
import classNames from 'classnames';
import { useEffect, useMemo } from 'react';
import TensorBoardStatusCell from '../TensorBoardStatus';
import styles from './index.less';

@@ -36,6 +39,25 @@ function ExperimentInstanceComponent({
onLoadMore,
}: ExperimentInstanceProps) {
const { message } = App.useApp();
const allIntanceIds = useMemo(() => {
return experimentInList?.map((item) => item.id) || [];
}, [experimentInList]);
const [
selectedIns,
setSelectedIns,
checked,
indeterminate,
checkAll,
isSingleChecked,
checkSingle,
] = useCheck(allIntanceIds);

useEffect(() => {
// 关闭时清空
if (allIntanceIds.length === 0) {
setSelectedIns([]);
}
}, [experimentInList]);

// 删除实验实例确认
const handleRemove = (instance: ExperimentInstance) => {
@@ -56,6 +78,26 @@ function ExperimentInstanceComponent({
}
};

// 批量删除实验实例确认
const handleDeleteAll = () => {
modalConfirm({
title: '确定批量删除选中的实例吗?',
onOk: () => {
batchDeleteExperimentInstances();
},
});
};

// 批量删除实验实例
const batchDeleteExperimentInstances = async () => {
const [res] = await to(deleteManyExperimentIns(selectedIns));
if (res) {
message.success('删除成功');
setSelectedIns([]);
onRemove?.();
}
};

// 终止实验实例
const terminateExperimentInstance = async (instance: ExperimentInstance) => {
const [res] = await to(putQueryByExperimentInsId(instance.id));
@@ -72,6 +114,9 @@ function ExperimentInstanceComponent({
return (
<div>
<div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}>
<div className={styles.check}>
<Checkbox checked={checked} indeterminate={indeterminate} onChange={checkAll}></Checkbox>
</div>
<div className={styles.index}>序号</div>
<div className={styles.tensorBoard}>可视化</div>
<div className={styles.description}>
@@ -79,7 +124,20 @@ function ExperimentInstanceComponent({
<div style={{ width: '50%' }}>开始时间</div>
</div>
<div className={styles.status}>状态</div>
<div className={styles.operation}>操作</div>
<div className={styles.operation}>
<span>操作</span>
{selectedIns.length > 0 && (
<Button
style={{ position: 'absolute', right: '0' }}
type="primary"
size="small"
onClick={handleDeleteAll}
icon={<KFIcon type="icon-shanchu" />}
>
删除
</Button>
)}
</div>
</div>

{experimentInList.map((item, index) => (
@@ -87,6 +145,12 @@ function ExperimentInstanceComponent({
key={item.id}
className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)}
>
<div className={styles.check}>
<Checkbox
checked={isSingleChecked(item.id)}
onChange={() => checkSingle(item.id)}
></Checkbox>
</div>
<a
className={styles.index}
style={{ padding: '0 16px' }}


+ 25
- 9
react-ui/src/pages/Experiment/index.jsx View File

@@ -60,7 +60,7 @@ function Experiment() {
};

useEffect(() => {
getList();
getExperimentList();
getWorkflowList();
return () => {
clearExperimentInTimers();
@@ -68,7 +68,7 @@ function Experiment() {
}, []);

// 获取实验列表
const getList = async () => {
const getExperimentList = async () => {
const params = {
offset: 0,
page: pageOption.current.page - 1,
@@ -228,8 +228,8 @@ function Experiment() {
setIsModalOpen(false);
};

// 创建或者编辑实验接口请求
const handleAddExperiment = async (values) => {
// 创建或者编辑实验
const handleAddExperiment = async (values, isRun) => {
const global_param = JSON.stringify(values.global_param);
if (!experimentId) {
const params = {
@@ -240,7 +240,7 @@ function Experiment() {
if (res) {
message.success('新建实验成功');
setIsModalOpen(false);
getList();
getExperimentList();
}
} else {
const params = { ...values, global_param, id: experimentId };
@@ -248,7 +248,12 @@ function Experiment() {
if (res) {
message.success('编辑实验成功');
setIsModalOpen(false);
getList();
getExperimentList();

// 确定并运行
if (isRun) {
runExperiment(experimentId);
}
}
}
};
@@ -259,7 +264,7 @@ function Experiment() {
page: current,
size: size,
};
getList();
getExperimentList();
};
// 运行实验
const runExperiment = async (id) => {
@@ -297,8 +302,16 @@ function Experiment() {
}
};

// 刷新实验列表状态,
// 目前是直接刷新实验列表,后续需要优化,只刷新状态
const refreshExperimentList = () => {
getExperimentList();
};

// 实验实例终止
const handleInstanceTerminate = async (experimentIn) => {
// 刷新实验列表
refreshExperimentList();
setExperimentInList((prevList) => {
return prevList.map((item) => {
if (item.id === experimentIn.id) {
@@ -448,7 +461,7 @@ function Experiment() {
deleteExperimentById(record.id).then((ret) => {
if (ret.code === 200) {
message.success('删除成功');
getList();
getExperimentList();
} else {
message.error(ret.msg);
}
@@ -485,7 +498,10 @@ function Experiment() {
experimentInsTotal={experimentInsTotal}
onClickInstance={(item) => gotoInstanceInfo(item, record)}
onClickTensorBoard={handleTensorboard}
onRemove={() => refreshExperimentIns(record.id)}
onRemove={() => {
refreshExperimentIns(record.id);
refreshExperimentList();
}}
onTerminate={handleInstanceTerminate}
onLoadMore={() => loadMoreExperimentIns()}
></ExperimentInstance>


Loading…
Cancel
Save