| @@ -5,7 +5,7 @@ | |||||
| */ | */ | ||||
| import { FormInstance } from 'antd'; | import { FormInstance } from 'antd'; | ||||
| import { debounce } from 'lodash'; | 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]); | }, [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 { | &__url { | ||||
| margin-bottom: 10px; | |||||
| margin-bottom: 10px !important; | |||||
| color: @text-color-secondary; | color: @text-color-secondary; | ||||
| font-size: 14px; | font-size: 14px; | ||||
| } | } | ||||
| @@ -2,7 +2,8 @@ import createExperimentIcon from '@/assets/img/create-experiment.png'; | |||||
| import editExperimentIcon from '@/assets/img/edit-experiment.png'; | import editExperimentIcon from '@/assets/img/edit-experiment.png'; | ||||
| import KFModal from '@/components/KFModal'; | import KFModal from '@/components/KFModal'; | ||||
| import { type PipelineGlobalParam } from '@/types'; | 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 { useState } from 'react'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| @@ -17,7 +18,7 @@ type AddExperimentModalProps = { | |||||
| isAdd: boolean; | isAdd: boolean; | ||||
| open: boolean; | open: boolean; | ||||
| onCancel: () => void; | onCancel: () => void; | ||||
| onFinish: () => void; | |||||
| onFinish: (values: any, isRun: boolean) => void; | |||||
| workflowList: Workflow[]; | workflowList: Workflow[]; | ||||
| initialValues: FormData; | initialValues: FormData; | ||||
| }; | }; | ||||
| @@ -113,25 +114,45 @@ function AddExperimentModal({ | |||||
| form.setFieldValue('global_param', []); | 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 ( | return ( | ||||
| <KFModal | <KFModal | ||||
| className={styles['add-experiment-modal']} | className={styles['add-experiment-modal']} | ||||
| title={modalTitle} | title={modalTitle} | ||||
| image={modalIcon} | image={modalIcon} | ||||
| open={open} | open={open} | ||||
| okButtonProps={{ | |||||
| htmlType: 'submit', | |||||
| form: 'form', | |||||
| }} | |||||
| onCancel={onCancel} | onCancel={onCancel} | ||||
| destroyOnClose={true} | destroyOnClose={true} | ||||
| width={825} | width={825} | ||||
| footer={footer} | |||||
| > | > | ||||
| <Form | <Form | ||||
| name="form" | name="form" | ||||
| layout="horizontal" | layout="horizontal" | ||||
| initialValues={initialValues} | initialValues={initialValues} | ||||
| onFinish={onFinish} | |||||
| autoComplete="off" | autoComplete="off" | ||||
| form={form} | form={form} | ||||
| {...layout} | {...layout} | ||||
| @@ -10,8 +10,12 @@ | |||||
| padding: 0 16px; | padding: 0 16px; | ||||
| } | } | ||||
| .check { | |||||
| width: calc((100% + 32px + 33px) / 6.25 / 2); | |||||
| } | |||||
| .index { | .index { | ||||
| width: calc((100% + 32px + 33px) / 6.25); | |||||
| width: calc((100% + 32px + 33px) / 6.25 / 2); | |||||
| } | } | ||||
| .tensorBoard { | .tensorBoard { | ||||
| @@ -33,6 +37,7 @@ | |||||
| } | } | ||||
| .operation { | .operation { | ||||
| position: relative; | |||||
| width: 344px; | width: 344px; | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,7 +1,9 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { ExperimentStatus } from '@/enums'; | import { ExperimentStatus } from '@/enums'; | ||||
| import { useCheck } from '@/hooks'; | |||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | import { experimentStatusInfo } from '@/pages/Experiment/status'; | ||||
| import { | import { | ||||
| deleteManyExperimentIns, | |||||
| deleteQueryByExperimentInsId, | deleteQueryByExperimentInsId, | ||||
| putQueryByExperimentInsId, | putQueryByExperimentInsId, | ||||
| } from '@/services/experiment/index.js'; | } from '@/services/experiment/index.js'; | ||||
| @@ -11,8 +13,9 @@ import { elapsedTime, formatDate } from '@/utils/date'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| import { DoubleRightOutlined } from '@ant-design/icons'; | 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 classNames from 'classnames'; | ||||
| import { useEffect, useMemo } from 'react'; | |||||
| import TensorBoardStatusCell from '../TensorBoardStatus'; | import TensorBoardStatusCell from '../TensorBoardStatus'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| @@ -36,6 +39,25 @@ function ExperimentInstanceComponent({ | |||||
| onLoadMore, | onLoadMore, | ||||
| }: ExperimentInstanceProps) { | }: ExperimentInstanceProps) { | ||||
| const { message } = App.useApp(); | 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) => { | 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 terminateExperimentInstance = async (instance: ExperimentInstance) => { | ||||
| const [res] = await to(putQueryByExperimentInsId(instance.id)); | const [res] = await to(putQueryByExperimentInsId(instance.id)); | ||||
| @@ -72,6 +114,9 @@ function ExperimentInstanceComponent({ | |||||
| return ( | return ( | ||||
| <div> | <div> | ||||
| <div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}> | <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.index}>序号</div> | ||||
| <div className={styles.tensorBoard}>可视化</div> | <div className={styles.tensorBoard}>可视化</div> | ||||
| <div className={styles.description}> | <div className={styles.description}> | ||||
| @@ -79,7 +124,20 @@ function ExperimentInstanceComponent({ | |||||
| <div style={{ width: '50%' }}>开始时间</div> | <div style={{ width: '50%' }}>开始时间</div> | ||||
| </div> | </div> | ||||
| <div className={styles.status}>状态</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> | </div> | ||||
| {experimentInList.map((item, index) => ( | {experimentInList.map((item, index) => ( | ||||
| @@ -87,6 +145,12 @@ function ExperimentInstanceComponent({ | |||||
| key={item.id} | key={item.id} | ||||
| className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)} | className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)} | ||||
| > | > | ||||
| <div className={styles.check}> | |||||
| <Checkbox | |||||
| checked={isSingleChecked(item.id)} | |||||
| onChange={() => checkSingle(item.id)} | |||||
| ></Checkbox> | |||||
| </div> | |||||
| <a | <a | ||||
| className={styles.index} | className={styles.index} | ||||
| style={{ padding: '0 16px' }} | style={{ padding: '0 16px' }} | ||||
| @@ -60,7 +60,7 @@ function Experiment() { | |||||
| }; | }; | ||||
| useEffect(() => { | useEffect(() => { | ||||
| getList(); | |||||
| getExperimentList(); | |||||
| getWorkflowList(); | getWorkflowList(); | ||||
| return () => { | return () => { | ||||
| clearExperimentInTimers(); | clearExperimentInTimers(); | ||||
| @@ -68,7 +68,7 @@ function Experiment() { | |||||
| }, []); | }, []); | ||||
| // 获取实验列表 | // 获取实验列表 | ||||
| const getList = async () => { | |||||
| const getExperimentList = async () => { | |||||
| const params = { | const params = { | ||||
| offset: 0, | offset: 0, | ||||
| page: pageOption.current.page - 1, | page: pageOption.current.page - 1, | ||||
| @@ -228,8 +228,8 @@ function Experiment() { | |||||
| setIsModalOpen(false); | setIsModalOpen(false); | ||||
| }; | }; | ||||
| // 创建或者编辑实验接口请求 | |||||
| const handleAddExperiment = async (values) => { | |||||
| // 创建或者编辑实验 | |||||
| const handleAddExperiment = async (values, isRun) => { | |||||
| const global_param = JSON.stringify(values.global_param); | const global_param = JSON.stringify(values.global_param); | ||||
| if (!experimentId) { | if (!experimentId) { | ||||
| const params = { | const params = { | ||||
| @@ -240,7 +240,7 @@ function Experiment() { | |||||
| if (res) { | if (res) { | ||||
| message.success('新建实验成功'); | message.success('新建实验成功'); | ||||
| setIsModalOpen(false); | setIsModalOpen(false); | ||||
| getList(); | |||||
| getExperimentList(); | |||||
| } | } | ||||
| } else { | } else { | ||||
| const params = { ...values, global_param, id: experimentId }; | const params = { ...values, global_param, id: experimentId }; | ||||
| @@ -248,7 +248,12 @@ function Experiment() { | |||||
| if (res) { | if (res) { | ||||
| message.success('编辑实验成功'); | message.success('编辑实验成功'); | ||||
| setIsModalOpen(false); | setIsModalOpen(false); | ||||
| getList(); | |||||
| getExperimentList(); | |||||
| // 确定并运行 | |||||
| if (isRun) { | |||||
| runExperiment(experimentId); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| }; | }; | ||||
| @@ -259,7 +264,7 @@ function Experiment() { | |||||
| page: current, | page: current, | ||||
| size: size, | size: size, | ||||
| }; | }; | ||||
| getList(); | |||||
| getExperimentList(); | |||||
| }; | }; | ||||
| // 运行实验 | // 运行实验 | ||||
| const runExperiment = async (id) => { | const runExperiment = async (id) => { | ||||
| @@ -297,8 +302,16 @@ function Experiment() { | |||||
| } | } | ||||
| }; | }; | ||||
| // 刷新实验列表状态, | |||||
| // 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||||
| const refreshExperimentList = () => { | |||||
| getExperimentList(); | |||||
| }; | |||||
| // 实验实例终止 | // 实验实例终止 | ||||
| const handleInstanceTerminate = async (experimentIn) => { | const handleInstanceTerminate = async (experimentIn) => { | ||||
| // 刷新实验列表 | |||||
| refreshExperimentList(); | |||||
| setExperimentInList((prevList) => { | setExperimentInList((prevList) => { | ||||
| return prevList.map((item) => { | return prevList.map((item) => { | ||||
| if (item.id === experimentIn.id) { | if (item.id === experimentIn.id) { | ||||
| @@ -448,7 +461,7 @@ function Experiment() { | |||||
| deleteExperimentById(record.id).then((ret) => { | deleteExperimentById(record.id).then((ret) => { | ||||
| if (ret.code === 200) { | if (ret.code === 200) { | ||||
| message.success('删除成功'); | message.success('删除成功'); | ||||
| getList(); | |||||
| getExperimentList(); | |||||
| } else { | } else { | ||||
| message.error(ret.msg); | message.error(ret.msg); | ||||
| } | } | ||||
| @@ -485,7 +498,10 @@ function Experiment() { | |||||
| experimentInsTotal={experimentInsTotal} | experimentInsTotal={experimentInsTotal} | ||||
| onClickInstance={(item) => gotoInstanceInfo(item, record)} | onClickInstance={(item) => gotoInstanceInfo(item, record)} | ||||
| onClickTensorBoard={handleTensorboard} | onClickTensorBoard={handleTensorboard} | ||||
| onRemove={() => refreshExperimentIns(record.id)} | |||||
| onRemove={() => { | |||||
| refreshExperimentIns(record.id); | |||||
| refreshExperimentList(); | |||||
| }} | |||||
| onTerminate={handleInstanceTerminate} | onTerminate={handleInstanceTerminate} | ||||
| onLoadMore={() => loadMoreExperimentIns()} | onLoadMore={() => loadMoreExperimentIns()} | ||||
| ></ExperimentInstance> | ></ExperimentInstance> | ||||