| @@ -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; | |||
| }; | |||
| @@ -39,7 +39,7 @@ | |||
| } | |||
| &__url { | |||
| margin-bottom: 10px; | |||
| margin-bottom: 10px !important; | |||
| color: @text-color-secondary; | |||
| font-size: 14px; | |||
| } | |||
| @@ -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} | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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' }} | |||
| @@ -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> | |||