| @@ -62,3 +62,7 @@ | |||
| font-size: 12px; | |||
| } | |||
| } | |||
| .parameter-input.parameter-input--error { | |||
| border-color: @error-color; | |||
| } | |||
| @@ -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,4 +1,4 @@ | |||
| .resource-select { | |||
| .kf-resource-select { | |||
| position: relative; | |||
| display: flex; | |||
| align-items: center; | |||
| @@ -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)} | |||
| @@ -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; | |||
| } | |||
| @@ -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; | |||
| @@ -42,7 +42,7 @@ const statusConfig: Record<TensorBoardStatus, TensorBoardStatusInfo> = { | |||
| }; | |||
| type TensorBoardStatusProps = { | |||
| status: TensorBoardStatus; | |||
| status?: TensorBoardStatus; | |||
| onClick: () => void; | |||
| }; | |||
| @@ -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); | |||
| }, | |||
| @@ -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 { | |||
| @@ -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" | |||
| /> | |||
| @@ -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" | |||
| @@ -336,6 +336,7 @@ function ModelDeployment() { | |||
| total: total, | |||
| showSizeChanger: true, | |||
| showQuickJumper: true, | |||
| showTotal: () => `共${total}条`, | |||
| }} | |||
| onChange={handleTableChange} | |||
| rowKey="service_id" | |||
| @@ -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) => { | |||
| @@ -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> | |||
| @@ -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删除实验实例 | |||
| @@ -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,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]; | |||
| } | |||
| } | |||