| @@ -10,13 +10,21 @@ type RunDurationProps = { | |||
| }; | |||
| function RunDuration({ createTime, finishTime, className, style }: RunDurationProps) { | |||
| const [now] = useServerTime(); | |||
| const [currentTime, setCurrentTime] = useState<Date>(now()); | |||
| const [currentTime, setCurrentTime] = useState<Date>(finishTime ? new Date(finishTime) : now()); | |||
| // console.log( | |||
| // 'currentTime', | |||
| // new Date(createTime ?? 0), | |||
| // currentTime, | |||
| // (currentTime.getTime() - new Date(createTime ?? 0).getTime()) / 1000, | |||
| // ); | |||
| // 定时刷新耗时 | |||
| useEffect(() => { | |||
| if (finishTime) { | |||
| setCurrentTime(new Date(finishTime)); | |||
| } else { | |||
| setCurrentTime(now()); | |||
| const timer = setInterval(() => { | |||
| setCurrentTime(now()); | |||
| }, 1000); | |||
| @@ -25,6 +33,7 @@ function RunDuration({ createTime, finishTime, className, style }: RunDurationPr | |||
| }; | |||
| } | |||
| }, [finishTime, now]); | |||
| return ( | |||
| <span className={className} style={style}> | |||
| {elapsedTime(createTime, currentTime)} | |||
| @@ -1,11 +1,24 @@ | |||
| import { parseJsonText } from '@/utils'; | |||
| import { useEffect } from 'react'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { NodeStatus } from '@/types'; | |||
| import { parseJsonText } from '@/utils'; | |||
| import { useEffect } from 'react'; | |||
| export type MessageHandler = (experimentInsId: number, status: string, finishedAt: string, nodes: Record<string, NodeStatus>) => void | |||
| export const useSSE = (experimentInsId: number, status: ExperimentStatus, name: string, namespace: string, onMessage: MessageHandler) => { | |||
| const isRunning = status === ExperimentStatus.Pending || status === ExperimentStatus.Running | |||
| export type MessageHandler = ( | |||
| experimentId: number, | |||
| experimentInsId: number, | |||
| status: string, | |||
| finishTime: string, | |||
| nodes: Record<string, NodeStatus>, | |||
| ) => void; | |||
| export const useSSE = ( | |||
| experimentId: number, | |||
| experimentInsId: number, | |||
| status: ExperimentStatus, | |||
| name: string, | |||
| namespace: string, | |||
| onMessage: MessageHandler, | |||
| ) => { | |||
| const isRunning = status === ExperimentStatus.Pending || status === ExperimentStatus.Running; | |||
| useEffect(() => { | |||
| if (isRunning) { | |||
| const { origin } = location; | |||
| @@ -22,8 +35,8 @@ export const useSSE = (experimentInsId: number, status: ExperimentStatus, name: | |||
| const dataJson = parseJsonText(data); | |||
| const statusData = dataJson?.result?.object?.status; | |||
| if (statusData) { | |||
| const { finishedAt, phase, nodes } = statusData; | |||
| onMessage(experimentInsId, phase, finishedAt, nodes); | |||
| const { finishedAt, phase, nodes } = statusData; | |||
| onMessage(experimentId, experimentInsId, phase, finishedAt, nodes); | |||
| } | |||
| }; | |||
| @@ -33,8 +46,7 @@ export const useSSE = (experimentInsId: number, status: ExperimentStatus, name: | |||
| return () => { | |||
| evtSource.close(); | |||
| } | |||
| }; | |||
| } | |||
| }, [experimentInsId, isRunning, name, namespace, onMessage]); | |||
| }, [experimentId, experimentInsId, isRunning, name, namespace, onMessage]); | |||
| }; | |||
| @@ -68,7 +68,7 @@ function ActiveLearnInstance() { | |||
| return; | |||
| } | |||
| // 设置节点状态 | |||
| // 设置总 workflow 状态 | |||
| const nodeStatusJson = parseJsonText(node_status); | |||
| if (nodeStatusJson) { | |||
| setNodes(nodeStatusJson); | |||
| @@ -105,18 +105,17 @@ function ActiveLearnInstance() { | |||
| if (dataJson) { | |||
| const nodes = dataJson?.result?.object?.status?.nodes; | |||
| if (nodes) { | |||
| // 节点 | |||
| // 设置节点 | |||
| setNodes(nodes); | |||
| // 设置总 workflow 状态 | |||
| const workflowStatus = Object.values(nodes).find((node: any) => | |||
| node.displayName.startsWith(NodePrefix), | |||
| ) as NodeStatus; | |||
| // 设置工作流状态 | |||
| if (workflowStatus) { | |||
| setWorkflowStatus(workflowStatus); | |||
| // 实验结束,关闭 SSE | |||
| // 实验结束,关闭 SSE,获取实验实例结果 | |||
| if ( | |||
| workflowStatus.phase !== ExperimentStatus.Pending && | |||
| workflowStatus.phase !== ExperimentStatus.Running | |||
| @@ -151,7 +150,7 @@ function ActiveLearnInstance() { | |||
| <ActiveLearnBasic | |||
| className={styles['active-learn-instance__basic']} | |||
| info={experimentInfo} | |||
| runStatus={workflowStatus} | |||
| workflowStatus={workflowStatus} | |||
| instanceStatus={instanceInfo?.status as ExperimentStatus} | |||
| isInstance | |||
| /> | |||
| @@ -28,14 +28,14 @@ type BasicInfoProps = { | |||
| info?: ActiveLearnData; | |||
| className?: string; | |||
| isInstance?: boolean; | |||
| runStatus?: NodeStatus; | |||
| workflowStatus?: NodeStatus; | |||
| instanceStatus?: ExperimentStatus; | |||
| }; | |||
| function BasicInfo({ | |||
| info, | |||
| className, | |||
| runStatus, | |||
| workflowStatus, | |||
| instanceStatus, | |||
| isInstance = false, | |||
| }: BasicInfoProps) { | |||
| @@ -212,8 +212,8 @@ function BasicInfo({ | |||
| return ( | |||
| <div className={classNames(styles['active-learn-basic'], className)}> | |||
| {isInstance && runStatus && ( | |||
| <ExperimentRunBasic runStatus={runStatus} instanceStatus={instanceStatus} /> | |||
| {isInstance && workflowStatus && ( | |||
| <ExperimentRunBasic workflowStatus={workflowStatus} instanceStatus={instanceStatus} /> | |||
| )} | |||
| {!isInstance && ( | |||
| <ConfigInfo | |||
| @@ -112,17 +112,17 @@ function AutoMLInstance() { | |||
| if (dataJson) { | |||
| const nodes = dataJson?.result?.object?.status?.nodes; | |||
| if (nodes) { | |||
| // 节点 | |||
| // 设置节点 | |||
| setNodes(nodes); | |||
| // 设置总 workflow 状态 | |||
| const workflowStatus = Object.values(nodes).find((node: any) => | |||
| node.displayName.startsWith(NodePrefix), | |||
| ) as NodeStatus; | |||
| if (workflowStatus) { | |||
| setWorkflowStatus(workflowStatus); | |||
| // 实验结束,关闭 SSE | |||
| // 实验结束,关闭 SSE,获取实验实例结果 | |||
| if ( | |||
| workflowStatus.phase !== ExperimentStatus.Pending && | |||
| workflowStatus.phase !== ExperimentStatus.Running | |||
| @@ -157,7 +157,7 @@ function AutoMLInstance() { | |||
| <AutoMLBasic | |||
| className={styles['auto-ml-instance__basic']} | |||
| info={autoMLInfo} | |||
| runStatus={workflowStatus} | |||
| workflowStatus={workflowStatus} | |||
| instanceStatus={instanceInfo?.status as ExperimentStatus} | |||
| isInstance | |||
| /> | |||
| @@ -38,7 +38,7 @@ type AutoMLBasicProps = { | |||
| info?: AutoMLData; | |||
| className?: string; | |||
| isInstance?: boolean; | |||
| runStatus?: NodeStatus; | |||
| workflowStatus?: NodeStatus; | |||
| instanceStatus?: ExperimentStatus; | |||
| instanceCreateTime?: string; | |||
| }; | |||
| @@ -46,7 +46,7 @@ type AutoMLBasicProps = { | |||
| function AutoMLBasic({ | |||
| info, | |||
| className, | |||
| runStatus, | |||
| workflowStatus, | |||
| instanceStatus, | |||
| isInstance = false, | |||
| }: AutoMLBasicProps) { | |||
| @@ -293,8 +293,8 @@ function AutoMLBasic({ | |||
| return ( | |||
| <div className={classNames(styles['auto-ml-basic'], className)}> | |||
| {isInstance && runStatus && ( | |||
| <ExperimentRunBasic runStatus={runStatus} instanceStatus={instanceStatus} /> | |||
| {isInstance && workflowStatus && ( | |||
| <ExperimentRunBasic workflowStatus={workflowStatus} instanceStatus={instanceStatus} /> | |||
| )} | |||
| {!isInstance && ( | |||
| <ConfigInfo | |||
| @@ -54,10 +54,6 @@ | |||
| display: flex; | |||
| align-items: center; | |||
| width: 200px; | |||
| .statusIcon { | |||
| visibility: visible; | |||
| } | |||
| } | |||
| } | |||
| @@ -182,7 +182,10 @@ function ExperimentInstanceList({ | |||
| > | |||
| {index + 1} | |||
| </a> | |||
| <ExperimentInstanceComponent instance={item}></ExperimentInstanceComponent> | |||
| <ExperimentInstanceComponent | |||
| experimentId={item[config['idInsProperty'] as keyof ExperimentInstance] as number} | |||
| instance={item} | |||
| ></ExperimentInstanceComponent> | |||
| <div className={styles.operation}> | |||
| <Button | |||
| type="link" | |||
| @@ -3,38 +3,41 @@ import { ExperimentStatus } from '@/enums'; | |||
| import { useSSE, type MessageHandler } from '@/hooks/useSSE'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { ExperimentInstance, NodeStatus } from '@/types'; | |||
| import { getWorkflowStatus } from '@/utils'; | |||
| import { ExperimentCompleted } from '@/utils/constant'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { getExperimentInstanceStatus, getWorkflowStatus } from '@/utils/experiment'; | |||
| import { Typography } from 'antd'; | |||
| import React, { useCallback } from 'react'; | |||
| import styles from './index.less'; | |||
| type ExperimentInstanceComponentProps = { | |||
| experimentId: number; | |||
| instance: ExperimentInstance; | |||
| }; | |||
| function ExperimentInstanceComponent({ instance }: ExperimentInstanceComponentProps) { | |||
| const { id, argo_ins_name, argo_ins_ns, node_status, create_time, finish_time } = instance; | |||
| const status = instance.status as ExperimentStatus; | |||
| function ExperimentInstanceComponent({ experimentId, instance }: ExperimentInstanceComponentProps) { | |||
| const { id, argo_ins_name, argo_ins_ns, node_status } = instance; | |||
| const workflowStatus = getWorkflowStatus(node_status) as NodeStatus | undefined; | |||
| const createTime = workflowStatus?.startedAt ?? create_time; | |||
| const finishTime = workflowStatus?.finishedAt ?? finish_time; | |||
| const status = getExperimentInstanceStatus(instance.status as ExperimentStatus); | |||
| const createTime = workflowStatus?.startedAt; | |||
| const finishTime = workflowStatus?.finishedAt; | |||
| const statusInfo = experimentStatusInfo[status]; | |||
| const handleSSEMessage: MessageHandler = useCallback( | |||
| (experimentInsId: number, status: string, finish_time: string) => { | |||
| (experimentId: number, experimentInsId: number, status: string, finishTime: string) => { | |||
| window.postMessage({ | |||
| type: ExperimentCompleted, | |||
| payload: { | |||
| id: experimentInsId, | |||
| experimentId, | |||
| experimentInsId, | |||
| status, | |||
| finish_time, | |||
| finishTime, | |||
| }, | |||
| }); | |||
| }, | |||
| [], | |||
| ); | |||
| useSSE(id, status, argo_ins_name, argo_ins_ns, handleSSEMessage); | |||
| useSSE(experimentId, id, status, argo_ins_name, argo_ins_ns, handleSSEMessage); | |||
| return ( | |||
| <React.Fragment> | |||
| @@ -47,15 +50,19 @@ function ExperimentInstanceComponent({ instance }: ExperimentInstanceComponentPr | |||
| </Typography.Text> | |||
| </div> | |||
| <div className={styles.statusBox}> | |||
| <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={experimentStatusInfo[status]?.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <span style={{ color: experimentStatusInfo[status]?.color }} className={styles.statusIcon}> | |||
| {experimentStatusInfo[status]?.label} | |||
| </span> | |||
| {statusInfo ? ( | |||
| <> | |||
| <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={statusInfo.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <span style={{ color: statusInfo.color }}>{statusInfo.label}</span> | |||
| </> | |||
| ) : ( | |||
| '--' | |||
| )} | |||
| </div> | |||
| </React.Fragment> | |||
| ); | |||
| @@ -8,6 +8,7 @@ import { | |||
| batchDeleteActiveLearnInsReq, | |||
| deleteActiveLearnInsReq, | |||
| deleteActiveLearnReq, | |||
| editActiveLearnInsReq, | |||
| getActiveLearnInsListReq, | |||
| getActiveLearnListReq, | |||
| runActiveLearnReq, | |||
| @@ -17,6 +18,7 @@ import { | |||
| batchDeleteExperimentInsReq, | |||
| deleteAutoMLReq, | |||
| deleteExperimentInsReq, | |||
| editExperimentInsReq, | |||
| getAutoMLListReq, | |||
| getExperimentInsListReq, | |||
| runAutoMLReq, | |||
| @@ -26,6 +28,7 @@ import { | |||
| batchDeleteRayInsReq, | |||
| deleteRayInsReq, | |||
| deleteRayReq, | |||
| editRayInsReq, | |||
| getRayInsListReq, | |||
| getRayListReq, | |||
| runRayReq, | |||
| @@ -40,17 +43,19 @@ export enum ExperimentListType { | |||
| type ExperimentListInfo = { | |||
| getListReq: (params: any, skipLoading?: boolean) => Promise<any>; // 获取列表 | |||
| getInsListReq: (params: any) => Promise<any>; // 获取实例列表 | |||
| getInsListReq: (params: any, skipLoading?: boolean) => Promise<any>; // 获取实例列表 | |||
| deleteRecordReq: (params: any) => Promise<any>; // 删除 | |||
| runRecordReq: (params: any) => Promise<any>; // 运行 | |||
| deleteInsReq: (params: any) => Promise<any>; // 删除实例 | |||
| batchDeleteInsReq: (params: any) => Promise<any>; // 批量删除实例 | |||
| stopInsReq: (params: any) => Promise<any>; // 终止实例 | |||
| editInsReq: (params: any) => Promise<any>; // 编辑实例 | |||
| title: string; // 标题 | |||
| pathPrefix: string; // 路由路径前缀 | |||
| idProperty: string; // ID属性 | |||
| nameProperty: string; // 名称属性 | |||
| descProperty: string; // 描述属性 | |||
| idInsProperty: string; // 实例返回的ID属性 | |||
| }; | |||
| export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo> = { | |||
| @@ -62,11 +67,13 @@ export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo | |||
| deleteInsReq: deleteExperimentInsReq, | |||
| batchDeleteInsReq: batchDeleteExperimentInsReq, | |||
| stopInsReq: stopExperimentInsReq, | |||
| editInsReq: editExperimentInsReq, | |||
| title: '自主机器学习', | |||
| pathPrefix: 'automl', | |||
| nameProperty: 'name', | |||
| descProperty: 'description', | |||
| idProperty: 'machineLearnId', | |||
| idInsProperty: 'machine_learn_id', | |||
| }, | |||
| [ExperimentListType.HyperParameter]: { | |||
| getListReq: getRayListReq, | |||
| @@ -76,11 +83,13 @@ export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo | |||
| deleteInsReq: deleteRayInsReq, | |||
| batchDeleteInsReq: batchDeleteRayInsReq, | |||
| stopInsReq: stopRayInsReq, | |||
| editInsReq: editRayInsReq, | |||
| title: '超参数自动寻优', | |||
| pathPrefix: 'hyperparameter', | |||
| nameProperty: 'name', | |||
| descProperty: 'description', | |||
| idProperty: 'rayId', | |||
| idInsProperty: 'ray_id', | |||
| }, | |||
| [ExperimentListType.ActiveLearn]: { | |||
| getListReq: getActiveLearnListReq, | |||
| @@ -90,10 +99,12 @@ export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo | |||
| deleteInsReq: deleteActiveLearnInsReq, | |||
| batchDeleteInsReq: batchDeleteActiveLearnInsReq, | |||
| stopInsReq: stopActiveLearnInsReq, | |||
| editInsReq: editActiveLearnInsReq, | |||
| title: '自动学习', | |||
| pathPrefix: 'active-learn', | |||
| nameProperty: 'name', | |||
| descProperty: 'description', | |||
| idProperty: 'activeLearnId', | |||
| idInsProperty: 'active_learn_id', | |||
| }, | |||
| }; | |||
| @@ -30,7 +30,7 @@ import { | |||
| } from 'antd'; | |||
| import { type SearchProps } from 'antd/es/input'; | |||
| import classNames from 'classnames'; | |||
| import { useCallback, useEffect, useRef, useState } from 'react'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| import ExperimentInstanceList from '../ExperimentInstanceList'; | |||
| import { ExperimentListType, experimentListConfig } from './config'; | |||
| import styles from './index.less'; | |||
| @@ -52,7 +52,6 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]); | |||
| const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]); | |||
| const [experimentInsTotal, setExperimentInsTotal] = useState(0); | |||
| const [now] = useServerTime(); | |||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||
| cacheState?.pagination ?? { | |||
| current: 1, | |||
| @@ -60,10 +59,10 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| }, | |||
| ); | |||
| const config = experimentListConfig[type]; | |||
| const timerRef = useRef<ReturnType<typeof window.setTimeout> | undefined>(); | |||
| const [now] = useServerTime(); | |||
| // 获取自主机器学习或超参数自动优化列表 | |||
| const getAutoMLList = useCallback( | |||
| // 获取实验列表 | |||
| const getExperimentList = useCallback( | |||
| async (skipLoading: boolean = false) => { | |||
| const params: Record<string, any> = { | |||
| page: pagination.current! - 1, | |||
| @@ -81,20 +80,16 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| [pagination, searchText, config], | |||
| ); | |||
| useEffect(() => { | |||
| getAutoMLList(); | |||
| }, [getAutoMLList]); | |||
| // 获取实验实例列表 | |||
| const getExperimentInsList = useCallback( | |||
| async (recordId: number, page: number, size: number) => { | |||
| async (recordId: number, page: number, size: number, skipLoading: boolean = false) => { | |||
| const params = { | |||
| [config.idProperty]: recordId, | |||
| page: page, | |||
| size: size, | |||
| }; | |||
| const request = config.getInsListReq; | |||
| const [res] = await to(request(params)); | |||
| const [res] = await to(request(params, skipLoading)); | |||
| if (res && res.data) { | |||
| const { content = [], totalElements = 0 } = res.data; | |||
| try { | |||
| @@ -116,60 +111,113 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| // TODO: 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||
| const refreshExperimentList = useCallback( | |||
| (skipLoading: boolean = false) => { | |||
| getAutoMLList(skipLoading); | |||
| getExperimentList(skipLoading); | |||
| }, | |||
| [getAutoMLList], | |||
| [getExperimentList], | |||
| ); | |||
| // 刷新实验实例列表 | |||
| const refreshExperimentIns = useCallback( | |||
| (experimentId: number) => { | |||
| (experimentId: number, skipLoading: boolean = false) => { | |||
| const length = experimentInsList.length; | |||
| getExperimentInsList(experimentId, 0, length); | |||
| getExperimentInsList(experimentId, 0, length, skipLoading); | |||
| }, | |||
| [getExperimentInsList, experimentInsList], | |||
| ); | |||
| // 新增,删除版本时,重置分页,然后刷新版本列表 | |||
| // 更新实验实例状态 | |||
| const editExperimentIns = useCallback( | |||
| async ( | |||
| experimentId: number, | |||
| experimentInsId: number, | |||
| status: ExperimentStatus, | |||
| argo_ins_name: string, | |||
| argo_ins_ns: string, | |||
| ) => { | |||
| const params = { | |||
| [config.idInsProperty]: experimentId, | |||
| id: experimentInsId, | |||
| status: status, | |||
| argo_ins_name, | |||
| argo_ins_ns, | |||
| }; | |||
| const request = config.editInsReq; | |||
| const [res] = await to(request(params)); | |||
| if (res && res.data) { | |||
| refreshExperimentIns(experimentId, true); | |||
| refreshExperimentList(true); | |||
| } | |||
| }, | |||
| [config, refreshExperimentIns, refreshExperimentList], | |||
| ); | |||
| // 获取实验列表 | |||
| useEffect(() => { | |||
| getExperimentList(); | |||
| }, [getExperimentList]); | |||
| // expandedRowKeys 变化 | |||
| useEffect(() => { | |||
| if (expandedRowKeys.length > 0) { | |||
| getExperimentInsList(expandedRowKeys[0], 0, 5); | |||
| refreshExperimentList(); | |||
| } | |||
| }, [expandedRowKeys, getExperimentInsList, refreshExperimentList]); | |||
| // 实验实例状态变化 | |||
| useEffect(() => { | |||
| const handleMessage = (e: MessageEvent) => { | |||
| const { type, payload } = e.data; | |||
| if (type === ExperimentCompleted) { | |||
| const { id, status, finish_time } = payload; | |||
| // 修改实例的状态和结束时间 | |||
| setExperimentInsList((prev) => | |||
| prev.map((v) => | |||
| v.id === id | |||
| ? { | |||
| ...v, | |||
| status: status, | |||
| finish_time: finish_time, | |||
| } | |||
| : v, | |||
| ), | |||
| const { experimentId, experimentInsId, status, finishTime } = payload; | |||
| const currentIns = experimentInsList.find((v) => v.id === experimentInsId); | |||
| console.log( | |||
| '实验实例状态变化', | |||
| currentIns?.status, | |||
| status, | |||
| experimentId, | |||
| experimentInsId, | |||
| finishTime, | |||
| ); | |||
| if (timerRef.current) { | |||
| clearTimeout(timerRef.current); | |||
| timerRef.current = undefined; | |||
| if ( | |||
| !currentIns || | |||
| currentIns.status === ExperimentStatus.Terminated || | |||
| currentIns.status === status | |||
| ) { | |||
| return; | |||
| } | |||
| timerRef.current = setTimeout(() => { | |||
| refreshExperimentList(true); | |||
| }, 10000); | |||
| // refreshExperimentList(true); | |||
| // refreshExperimentIns(experimentId); | |||
| editExperimentIns( | |||
| experimentId, | |||
| experimentInsId, | |||
| status, | |||
| currentIns.argo_ins_name, | |||
| currentIns.argo_ins_ns, | |||
| ); | |||
| // 修改实例的状态和结束时间 | |||
| // setExperimentInsList((prev) => | |||
| // prev.map((v) => | |||
| // v.id === experimentInsId | |||
| // ? { | |||
| // ...v, | |||
| // status: status, | |||
| // finish_time: finishTime, | |||
| // } | |||
| // : v, | |||
| // ), | |||
| // ); | |||
| } | |||
| }; | |||
| window.addEventListener('message', handleMessage); | |||
| return () => { | |||
| window.removeEventListener('message', handleMessage); | |||
| if (timerRef.current) { | |||
| clearTimeout(timerRef.current); | |||
| timerRef.current = undefined; | |||
| } | |||
| }; | |||
| }, [refreshExperimentList]); | |||
| }, [experimentInsList, editExperimentIns]); | |||
| // 搜索 | |||
| const onSearch: SearchProps['onSearch'] = (value) => { | |||
| @@ -213,6 +261,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| setCacheState({ | |||
| pagination, | |||
| searchText, | |||
| expandedRowKeys, | |||
| }); | |||
| if (record) { | |||
| @@ -231,6 +280,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| setCacheState({ | |||
| pagination, | |||
| searchText, | |||
| expandedRowKeys, | |||
| }); | |||
| navigate(`info/${record.id}`); | |||
| @@ -243,8 +293,8 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| if (res) { | |||
| message.success('运行成功'); | |||
| setExpandedRowKeys([record.id]); | |||
| refreshExperimentList(); | |||
| getExperimentInsList(record.id, 0, 5); | |||
| // getExperimentInsList(record.id, 0, 5); | |||
| // refreshExperimentList(); | |||
| } | |||
| }; | |||
| @@ -254,8 +304,8 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| setExperimentInsList([]); | |||
| if (expanded) { | |||
| setExpandedRowKeys([record.id]); | |||
| getExperimentInsList(record.id, 0, 5); | |||
| refreshExperimentList(); | |||
| // getExperimentInsList(record.id, 0, 5); | |||
| // refreshExperimentList(); | |||
| } else { | |||
| setExpandedRowKeys([]); | |||
| } | |||
| @@ -263,6 +313,11 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| // 跳转到实验实例详情 | |||
| const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => { | |||
| setCacheState({ | |||
| pagination, | |||
| searchText, | |||
| expandedRowKeys, | |||
| }); | |||
| navigate(`instance/${autoML.id}/${record.id}`); | |||
| }; | |||
| @@ -275,8 +330,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| // 实验实例终止 | |||
| const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => { | |||
| // 刷新实验列表 | |||
| refreshExperimentList(); | |||
| // 修改实例的状态和结束时间 | |||
| setExperimentInsList((prevList) => { | |||
| return prevList.map((item) => { | |||
| if (item.id === experimentIns.id) { | |||
| @@ -289,6 +343,11 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| return item; | |||
| }); | |||
| }); | |||
| // 刷新实验列表 | |||
| refreshExperimentList(true); | |||
| if (expandedRowKeys.length > 0) { | |||
| refreshExperimentIns(expandedRowKeys[0]); | |||
| } | |||
| }; | |||
| // --------------------------- Table --------------------------- | |||
| // 分页切换 | |||
| @@ -3,58 +3,61 @@ import RunDuration from '@/components/RunDuration'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { type NodeStatus } from '@/types'; | |||
| import { getExperimentInstanceStatus } from '@/utils/experiment'; | |||
| import { formatDate } from '@/utils/format'; | |||
| import { Flex } from 'antd'; | |||
| import { useMemo } from 'react'; | |||
| type ExperimentRunBasicProps = { | |||
| runStatus?: NodeStatus; | |||
| workflowStatus?: NodeStatus; | |||
| instanceStatus?: ExperimentStatus; | |||
| }; | |||
| function ExperimentRunBasic({ runStatus, instanceStatus }: ExperimentRunBasicProps) { | |||
| function ExperimentRunBasic({ workflowStatus, instanceStatus }: ExperimentRunBasicProps) { | |||
| const instanceDatas = useMemo(() => { | |||
| if (!runStatus) { | |||
| return []; | |||
| } | |||
| const status = | |||
| instanceStatus === ExperimentStatus.Terminated ? instanceStatus : runStatus.phase; | |||
| const status = getExperimentInstanceStatus(instanceStatus as ExperimentStatus, workflowStatus); | |||
| const statusInfo = experimentStatusInfo[status]; | |||
| return [ | |||
| { | |||
| label: '启动时间', | |||
| value: formatDate(runStatus.startedAt), | |||
| value: formatDate(workflowStatus?.startedAt), | |||
| }, | |||
| { | |||
| label: '执行时长', | |||
| value: <RunDuration createTime={runStatus.startedAt} finishTime={runStatus.finishedAt} />, | |||
| value: ( | |||
| <RunDuration | |||
| createTime={workflowStatus?.startedAt} | |||
| finishTime={workflowStatus?.finishedAt} | |||
| /> | |||
| ), | |||
| }, | |||
| { | |||
| label: '状态', | |||
| value: ( | |||
| value: statusInfo ? ( | |||
| <Flex align="center"> | |||
| <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={statusInfo?.icon} | |||
| src={statusInfo.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <div | |||
| style={{ | |||
| color: statusInfo?.color, | |||
| color: statusInfo.color, | |||
| fontSize: '15px', | |||
| lineHeight: 1.6, | |||
| }} | |||
| > | |||
| {statusInfo?.label} | |||
| {statusInfo.label} | |||
| </div> | |||
| </Flex> | |||
| ) : ( | |||
| '--' | |||
| ), | |||
| }, | |||
| ]; | |||
| }, [runStatus, instanceStatus]); | |||
| }, [workflowStatus, instanceStatus]); | |||
| return ( | |||
| <ConfigInfo | |||
| @@ -7,6 +7,7 @@ import { getWorkflowById } from '@/services/pipeline/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { fittingString, parseJsonText } from '@/utils'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { getExperimentInstanceStatus } from '@/utils/experiment'; | |||
| import { to } from '@/utils/promise'; | |||
| import G6, { Util } from '@antv/g6'; | |||
| import { Button } from 'antd'; | |||
| @@ -34,6 +35,8 @@ function ExperimentText() { | |||
| const evtSourceRef = useRef(); | |||
| const width = 110; | |||
| const height = 36; | |||
| const status = getExperimentInstanceStatus(experimentIns?.status, workflowStatus); | |||
| const statusInfo = experimentStatusInfo[status]; | |||
| useEffect(() => { | |||
| initGraph(); | |||
| @@ -92,16 +95,17 @@ function ExperimentText() { | |||
| const workflowData = workflowRef.current; | |||
| const experimentStatusObjs = parseJsonText(nodes_status); | |||
| if (experimentStatusObjs) { | |||
| // 更新各个节点 | |||
| workflowData.nodes.forEach((item) => { | |||
| const experimentNode = experimentStatusObjs?.[item.id]; | |||
| updateWorkflowNode(item, experimentNode); | |||
| }); | |||
| // 处理workflow状态 | |||
| // 设置 workflow 总状态 | |||
| Object.keys(experimentStatusObjs).some((key) => { | |||
| if (key.startsWith(NodePrefix)) { | |||
| const workflowStatus = experimentStatusObjs[key]; | |||
| setWorkflowStatus(workflowStatus); | |||
| const tempWorkflowStatus = experimentStatusObjs[key]; | |||
| setWorkflowStatus(tempWorkflowStatus); | |||
| return true; | |||
| } | |||
| return false; | |||
| @@ -154,27 +158,30 @@ function ExperimentText() { | |||
| if (!statusData) { | |||
| return; | |||
| } | |||
| const { startedAt, finishedAt, phase, nodes = {} } = statusData; | |||
| const { finishedAt, phase, nodes = {} } = statusData; | |||
| // 更新实验实例状态和结束时间 | |||
| setExperimentIns((prev) => ({ | |||
| ...prev, | |||
| finish_time: finishedAt, | |||
| status: phase, | |||
| })); | |||
| const workflowStatus = Object.values(nodes).find((node) => | |||
| // 设置总 workflow 状态 | |||
| const tempWorkflowStatus = Object.values(nodes).find((node) => | |||
| node.displayName.startsWith(NodePrefix), | |||
| ); | |||
| // 设置工作流状态 | |||
| if (workflowStatus) { | |||
| setWorkflowStatus(workflowStatus); | |||
| if (tempWorkflowStatus) { | |||
| setWorkflowStatus(tempWorkflowStatus); | |||
| } | |||
| // 更新各个节点 | |||
| const workflowData = workflowRef.current; | |||
| workflowData.nodes.forEach((item) => { | |||
| const experimentNode = Object.values(nodes).find((node) => node.displayName === item.id); | |||
| updateWorkflowNode(item, experimentNode); | |||
| }); | |||
| // 绘制图 | |||
| getGraphData(workflowData, false); | |||
| // 更新打开的抽屉数据 | |||
| @@ -200,6 +207,7 @@ function ExperimentText() { | |||
| evtSourceRef.current = evtSource; | |||
| }; | |||
| // 更新各个节点 | |||
| function updateWorkflowNode(workflowNode, statusNode) { | |||
| if (!statusNode) { | |||
| return; | |||
| @@ -505,18 +513,19 @@ function ExperimentText() { | |||
| </div> | |||
| <div className={styles['pipeline-container__top__info']}> | |||
| 状态: | |||
| <div | |||
| style={{ | |||
| width: '8px', | |||
| height: '8px', | |||
| borderRadius: '50%', | |||
| marginRight: '6px', | |||
| backgroundColor: experimentStatusInfo[workflowStatus?.phase]?.color, | |||
| }} | |||
| ></div> | |||
| <span style={{ color: experimentStatusInfo[workflowStatus?.phase]?.color }}> | |||
| {experimentStatusInfo[workflowStatus?.phase]?.label} | |||
| </span> | |||
| {statusInfo ? ( | |||
| <> | |||
| <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={statusInfo.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <span style={{ color: statusInfo.color }}>{statusInfo.label}</span> | |||
| </> | |||
| ) : ( | |||
| '--' | |||
| )} | |||
| </div> | |||
| <Button | |||
| className={styles['pipeline-container__top__param-button']} | |||
| @@ -55,10 +55,6 @@ | |||
| display: flex; | |||
| align-items: center; | |||
| width: 160px; | |||
| .statusIcon { | |||
| visibility: visible; | |||
| } | |||
| } | |||
| } | |||
| @@ -3,9 +3,9 @@ import { ExperimentStatus } from '@/enums'; | |||
| import { useSSE, type MessageHandler } from '@/hooks/useSSE'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { ExperimentInstance, NodeStatus } from '@/types'; | |||
| import { getWorkflowStatus } from '@/utils'; | |||
| import { ExperimentCompleted } from '@/utils/constant'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { getExperimentInstanceStatus, getWorkflowStatus } from '@/utils/experiment'; | |||
| import { Typography } from 'antd'; | |||
| import React, { useCallback } from 'react'; | |||
| import styles from './index.less'; | |||
| @@ -15,26 +15,29 @@ type ExperimentInstanceComponentProps = { | |||
| }; | |||
| function ExperimentInstanceComponent({ instance }: ExperimentInstanceComponentProps) { | |||
| const { id, argo_ins_name, argo_ins_ns, nodes_status, create_time, finish_time } = instance; | |||
| const status = instance.status as ExperimentStatus; | |||
| const { id, experiment_id, argo_ins_name, argo_ins_ns, nodes_status, create_time, finish_time } = | |||
| instance; | |||
| const workflowStatus = getWorkflowStatus(nodes_status) as NodeStatus | undefined; | |||
| const status = getExperimentInstanceStatus(instance.status as ExperimentStatus); | |||
| const createTime = workflowStatus?.startedAt ?? create_time; | |||
| const finishTime = workflowStatus?.finishedAt ?? finish_time; | |||
| const statusInfo = experimentStatusInfo[status]; | |||
| const handleSSEMessage: MessageHandler = useCallback( | |||
| (experimentInsId: number, status: string, finish_time: string) => { | |||
| (experimentId: number, experimentInsId: number, status: string, finishTime: string) => { | |||
| window.postMessage({ | |||
| type: ExperimentCompleted, | |||
| payload: { | |||
| id: experimentInsId, | |||
| experimentId, | |||
| experimentInsId, | |||
| status, | |||
| finish_time, | |||
| finishTime, | |||
| }, | |||
| }); | |||
| }, | |||
| [], | |||
| ); | |||
| useSSE(id, status, argo_ins_name, argo_ins_ns, handleSSEMessage); | |||
| useSSE(experiment_id, id, status, argo_ins_name, argo_ins_ns, handleSSEMessage); | |||
| return ( | |||
| <React.Fragment> | |||
| @@ -49,15 +52,19 @@ function ExperimentInstanceComponent({ instance }: ExperimentInstanceComponentPr | |||
| </div> | |||
| </div> | |||
| <div className={styles.statusBox}> | |||
| <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={experimentStatusInfo[status]?.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <span style={{ color: experimentStatusInfo[status]?.color }} className={styles.statusIcon}> | |||
| {experimentStatusInfo[status]?.label} | |||
| </span> | |||
| {statusInfo ? ( | |||
| <> | |||
| <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={statusInfo.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <span style={{ color: statusInfo.color }}>{statusInfo.label}</span> | |||
| </> | |||
| ) : ( | |||
| '--' | |||
| )} | |||
| </div> | |||
| </React.Fragment> | |||
| ); | |||
| @@ -5,6 +5,7 @@ import { useCacheState } from '@/hooks/useCacheState'; | |||
| import { useServerTime } from '@/hooks/useServerTime'; | |||
| import { | |||
| deleteExperimentById, | |||
| editExperimentInsReq, | |||
| getExperiment, | |||
| getExperimentById, | |||
| getQueryByExperimentId, | |||
| @@ -40,7 +41,7 @@ function Experiment() { | |||
| const [workflowList, setWorkflowList] = useState([]); | |||
| const [experimentId, setExperimentId] = useState(null); | |||
| const [experimentInsList, setExperimentInsList] = useState([]); | |||
| const [expandedRowKeys, setExpandedRowKeys] = useState(null); | |||
| const [expandedRowKeys, setExpandedRowKeys] = useState([]); | |||
| const [total, setTotal] = useState(0); | |||
| const [isAdd, setIsAdd] = useState(true); | |||
| const [isModalOpen, setIsModalOpen] = useState(false); | |||
| @@ -61,7 +62,7 @@ function Experiment() { | |||
| // 获取实验列表 | |||
| const getExperimentList = useCallback( | |||
| async (skipLoading) => { | |||
| async (skipLoading = false) => { | |||
| const params = { | |||
| page: pagination.current - 1, | |||
| size: pagination.pageSize, | |||
| @@ -84,12 +85,114 @@ function Experiment() { | |||
| // 刷新实验列表状态, | |||
| // 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||
| const refreshExperimentList = useCallback( | |||
| (skipLoading) => { | |||
| (skipLoading = false) => { | |||
| getExperimentList(skipLoading); | |||
| }, | |||
| [getExperimentList], | |||
| ); | |||
| // 获取 TensorBoard 状态 | |||
| const getTensorBoardStatus = useCallback(async (experimentIn) => { | |||
| const params = { | |||
| namespace: experimentIn.nodes_result.tensorboard_log.namespace, | |||
| path: experimentIn.nodes_result.tensorboard_log.path, | |||
| pvc_name: experimentIn.nodes_result.tensorboard_log.pvc_name, | |||
| }; | |||
| const [res] = await to(getTensorBoardStatusReq(params)); | |||
| if (res && res.data) { | |||
| setExperimentInsList((prevList) => { | |||
| return prevList.map((item) => { | |||
| if (item.id === experimentIn.id) { | |||
| return { | |||
| ...item, | |||
| tensorBoardStatus: res.data.status, | |||
| tensorboardUrl: res.data.url, | |||
| }; | |||
| } | |||
| return item; | |||
| }); | |||
| }); | |||
| let timerId = timerIds.get(experimentIn.id); | |||
| if (timerId) { | |||
| clearTimeout(timerId); | |||
| timerIds.delete(experimentIn.id); | |||
| } | |||
| timerId = setTimeout(() => { | |||
| getTensorBoardStatus(experimentIn); | |||
| }, 10 * 1000); | |||
| timerIds.set(experimentIn.id, timerId); | |||
| } | |||
| }, []); | |||
| // 获取实验实例列表 | |||
| const getExperimentInsList = useCallback( | |||
| async (experimentId, page, size = 5, skipLoading = false) => { | |||
| const params = { | |||
| experimentId: experimentId, | |||
| page: page, | |||
| size: size, | |||
| }; | |||
| const [res, error] = await to(getQueryByExperimentId(params, skipLoading)); | |||
| if (res && res.data) { | |||
| const { content = [], totalElements = 0 } = res.data; | |||
| try { | |||
| const list = content.map((v) => { | |||
| const nodes_result = v.nodes_result ? JSON.parse(v.nodes_result) : {}; | |||
| return { | |||
| ...v, | |||
| nodes_result, | |||
| }; | |||
| }); | |||
| if (page === 0) { | |||
| setExperimentInsList(list); | |||
| clearExperimentInTimers(); | |||
| } else { | |||
| setExperimentInsList((prev) => [...prev, ...list]); | |||
| } | |||
| setExperimentInsTotal(totalElements); | |||
| // 获取 TensorBoard 状态 | |||
| list.forEach((item) => { | |||
| if (item.nodes_result?.tensorboard_log) { | |||
| getTensorBoardStatus(item); | |||
| } | |||
| }); | |||
| } catch (error) { | |||
| console.error('JSON parse error: ', error); | |||
| } | |||
| } | |||
| }, | |||
| [getTensorBoardStatus], | |||
| ); | |||
| // 刷新实验实例列表 | |||
| const refreshExperimentIns = useCallback( | |||
| (experimentId, skipLoading = false) => { | |||
| const length = experimentInsList.length; | |||
| getExperimentInsList(experimentId, 0, length, skipLoading); | |||
| }, | |||
| [experimentInsList, getExperimentInsList], | |||
| ); | |||
| // 更新实验状态 | |||
| const editExperimentIns = useCallback( | |||
| async (experimentId, experimentInsId, status, argo_ins_name, argo_ins_ns) => { | |||
| const params = { | |||
| experiment_id: experimentId, | |||
| id: experimentInsId, | |||
| status: status, | |||
| argo_ins_name, | |||
| argo_ins_ns, | |||
| }; | |||
| const [res, error] = await to(editExperimentInsReq(params)); | |||
| if (res && res.data) { | |||
| refreshExperimentIns(experimentId, true); | |||
| refreshExperimentList(true); | |||
| } | |||
| }, | |||
| [refreshExperimentIns, refreshExperimentList], | |||
| ); | |||
| // 获取流水线列表 | |||
| useEffect(() => { | |||
| // 获取流水线列表 | |||
| @@ -111,52 +214,66 @@ function Experiment() { | |||
| clearExperimentInTimers(); | |||
| }; | |||
| }, []); | |||
| // 获取实验列表 | |||
| useEffect(() => { | |||
| getExperimentList(); | |||
| }, [getExperimentList]); | |||
| // 新增,删除版本时,重置分页,然后刷新版本列表 | |||
| // 更新实验实例状态 | |||
| useEffect(() => { | |||
| const handleMessage = (e) => { | |||
| const { type, payload } = e.data; | |||
| if (type === ExperimentCompleted) { | |||
| const { id, status, finish_time } = payload; | |||
| // 修改实例的状态和结束时间 | |||
| setExperimentInsList((prev) => | |||
| prev.map((v) => | |||
| v.id === id | |||
| ? { | |||
| ...v, | |||
| status: status, | |||
| finish_time: finish_time, | |||
| } | |||
| : v, | |||
| ), | |||
| const { experimentId, experimentInsId, status, finishTime } = payload; | |||
| const currentIns = experimentInsList.find((v) => v.id === experimentInsId); | |||
| console.log( | |||
| '实验实例状态变化', | |||
| currentIns?.status, | |||
| status, | |||
| experimentId, | |||
| experimentInsId, | |||
| finishTime, | |||
| ); | |||
| if (timerRef.current) { | |||
| clearTimeout(timerRef.current); | |||
| timerRef.current = undefined; | |||
| if ( | |||
| !currentIns || | |||
| currentIns.status === ExperimentStatus.Terminated || | |||
| currentIns.status === status | |||
| ) { | |||
| return; | |||
| } | |||
| timerRef.current = setTimeout(() => { | |||
| refreshExperimentList(true); | |||
| }, 10000); | |||
| editExperimentIns( | |||
| experimentId, | |||
| experimentInsId, | |||
| status, | |||
| currentIns.argo_ins_name, | |||
| currentIns.argo_ins_ns, | |||
| ); | |||
| // refreshExperimentList(true); | |||
| // refreshExperimentIns(experimentId); | |||
| // 修改实例的状态和结束时间 | |||
| // setExperimentInsList((prev) => | |||
| // prev.map((v) => | |||
| // v.id === experimentInsId | |||
| // ? { | |||
| // ...v, | |||
| // status: status, | |||
| // finish_time: finishTime, | |||
| // } | |||
| // : v, | |||
| // ), | |||
| // ); | |||
| } | |||
| }; | |||
| window.addEventListener('message', handleMessage); | |||
| return () => { | |||
| window.removeEventListener('message', handleMessage); | |||
| if (timerRef.current) { | |||
| clearTimeout(timerRef.current); | |||
| timerRef.current = undefined; | |||
| } | |||
| }; | |||
| }, [refreshExperimentList]); | |||
| }, [experimentInsList, editExperimentIns]); | |||
| // 搜索 | |||
| const onSearch = (value) => { | |||
| @@ -167,44 +284,6 @@ function Experiment() { | |||
| })); | |||
| }; | |||
| // 获取实验实例列表 | |||
| const getQueryByExperiment = async (experimentId, page, size = 5) => { | |||
| const params = { | |||
| experimentId: experimentId, | |||
| page: page, | |||
| size: size, | |||
| }; | |||
| 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) { | |||
| setExperimentInsList(list); | |||
| clearExperimentInTimers(); | |||
| } else { | |||
| setExperimentInsList((prev) => [...prev, ...list]); | |||
| } | |||
| setExperimentInsTotal(totalElements); | |||
| // 获取 TensorBoard 状态 | |||
| list.forEach((item) => { | |||
| if (item.nodes_result?.tensorboard_log) { | |||
| getTensorBoardStatus(item); | |||
| } | |||
| }); | |||
| } catch (error) { | |||
| console.error('JSON parse error: ', error); | |||
| } | |||
| } | |||
| }; | |||
| // 运行 TensorBoard | |||
| const runTensorBoard = async (experimentIn) => { | |||
| const params = { | |||
| @@ -224,49 +303,16 @@ function Experiment() { | |||
| } | |||
| }; | |||
| // 获取 TensorBoard 状态 | |||
| const getTensorBoardStatus = async (experimentIn) => { | |||
| const params = { | |||
| namespace: experimentIn.nodes_result.tensorboard_log.namespace, | |||
| path: experimentIn.nodes_result.tensorboard_log.path, | |||
| pvc_name: experimentIn.nodes_result.tensorboard_log.pvc_name, | |||
| }; | |||
| const [res] = await to(getTensorBoardStatusReq(params)); | |||
| if (res && res.data) { | |||
| setExperimentInsList((prevList) => { | |||
| return prevList.map((item) => { | |||
| if (item.id === experimentIn.id) { | |||
| return { | |||
| ...item, | |||
| tensorBoardStatus: res.data.status, | |||
| tensorboardUrl: res.data.url, | |||
| }; | |||
| } | |||
| return item; | |||
| }); | |||
| }); | |||
| let timerId = timerIds.get(experimentIn.id); | |||
| if (timerId) { | |||
| clearTimeout(timerId); | |||
| timerIds.delete(experimentIn.id); | |||
| } | |||
| timerId = setTimeout(() => { | |||
| getTensorBoardStatus(experimentIn); | |||
| }, 10 * 1000); | |||
| timerIds.set(experimentIn.id, timerId); | |||
| } | |||
| }; | |||
| // 展开实例 | |||
| const expandChange = (e, record) => { | |||
| const expandChange = (expanded, record) => { | |||
| clearExperimentInTimers(); | |||
| setExperimentInsList([]); | |||
| if (record.id === expandedRowKeys) { | |||
| setExpandedRowKeys(null); | |||
| } else { | |||
| getQueryByExperiment(record.id, 0, 5); | |||
| if (expanded) { | |||
| setExpandedRowKeys([record.id]); | |||
| getExperimentInsList(record.id, 0, 5); | |||
| refreshExperimentList(); | |||
| } else { | |||
| setExpandedRowKeys([]); | |||
| } | |||
| }; | |||
| @@ -344,8 +390,9 @@ function Experiment() { | |||
| const [res] = await to(runExperiments(id)); | |||
| if (res) { | |||
| message.success('运行成功'); | |||
| setExpandedRowKeys([id]); | |||
| refreshExperimentList(); | |||
| getQueryByExperiment(id, 0, 5); | |||
| getExperimentInsList(id, 0, 5); | |||
| } | |||
| }; | |||
| @@ -388,8 +435,6 @@ function Experiment() { | |||
| // 实验实例终止 | |||
| const handleInstanceTerminate = async (experimentIn) => { | |||
| // 刷新实验列表 | |||
| refreshExperimentList(); | |||
| setExperimentInsList((prevList) => { | |||
| return prevList.map((item) => { | |||
| if (item.id === experimentIn.id) { | |||
| @@ -402,6 +447,9 @@ function Experiment() { | |||
| return item; | |||
| }); | |||
| }); | |||
| // 刷新实验列表 | |||
| refreshExperimentList(true); | |||
| refreshExperimentIns(experimentIn.experiment_id); | |||
| }; | |||
| // 实验对比菜单 | |||
| @@ -423,16 +471,10 @@ function Experiment() { | |||
| }; | |||
| }; | |||
| // 刷新实验实例列表 | |||
| const refreshExperimentIns = (experimentId) => { | |||
| const length = experimentInsList.length; | |||
| getQueryByExperiment(experimentId, 0, length); | |||
| }; | |||
| // 加载更多实验实例 | |||
| const loadMoreExperimentIns = () => { | |||
| const page = Math.round(experimentInsList.length / 5); | |||
| getQueryByExperiment(expandedRowKeys, page, 5); | |||
| getExperimentInsList(expandedRowKeys, page, 5); | |||
| }; | |||
| // 处理删除 | |||
| @@ -617,7 +659,7 @@ function Experiment() { | |||
| ></ExperimentInstanceList> | |||
| ), | |||
| onExpand: expandChange, | |||
| expandedRowKeys: [expandedRowKeys], | |||
| expandedRowKeys: expandedRowKeys, | |||
| }} | |||
| /> | |||
| </div> | |||
| @@ -120,18 +120,17 @@ function HyperParameterInstance() { | |||
| if (dataJson) { | |||
| const nodes = dataJson?.result?.object?.status?.nodes; | |||
| if (nodes) { | |||
| // 节点 | |||
| // 设置节点 | |||
| setNodes(nodes); | |||
| // 设置总 workflow 状态 | |||
| const workflowStatus = Object.values(nodes).find((node: any) => | |||
| node.displayName.startsWith(NodePrefix), | |||
| ) as NodeStatus; | |||
| // 设置工作流状态 | |||
| if (workflowStatus) { | |||
| setWorkflowStatus(workflowStatus); | |||
| // 实验结束,关闭 SSE | |||
| // 实验结束,关闭 SSE,获取实验实例结果 | |||
| if ( | |||
| workflowStatus.phase !== ExperimentStatus.Pending && | |||
| workflowStatus.phase !== ExperimentStatus.Running | |||
| @@ -166,7 +165,7 @@ function HyperParameterInstance() { | |||
| <HyperParameterBasic | |||
| className={styles['hyper-parameter-instance__basic']} | |||
| info={experimentInfo} | |||
| runStatus={workflowStatus} | |||
| workflowStatus={workflowStatus} | |||
| instanceStatus={instanceInfo?.status as ExperimentStatus} | |||
| isInstance | |||
| /> | |||
| @@ -30,14 +30,14 @@ type HyperParameterBasicProps = { | |||
| info?: HyperParameterData; | |||
| className?: string; | |||
| isInstance?: boolean; | |||
| runStatus?: NodeStatus; | |||
| workflowStatus?: NodeStatus; | |||
| instanceStatus?: ExperimentStatus; | |||
| }; | |||
| function HyperParameterBasic({ | |||
| info, | |||
| className, | |||
| runStatus, | |||
| workflowStatus, | |||
| instanceStatus, | |||
| isInstance = false, | |||
| }: HyperParameterBasicProps) { | |||
| @@ -144,8 +144,8 @@ function HyperParameterBasic({ | |||
| return ( | |||
| <div className={classNames(styles['hyper-parameter-basic'], className)}> | |||
| {isInstance && runStatus && ( | |||
| <ExperimentRunBasic runStatus={runStatus} instanceStatus={instanceStatus} /> | |||
| {isInstance && workflowStatus && ( | |||
| <ExperimentRunBasic workflowStatus={workflowStatus} instanceStatus={instanceStatus} /> | |||
| )} | |||
| {!isInstance && ( | |||
| <ConfigInfo | |||
| @@ -16,7 +16,7 @@ function AssetsManagement() { | |||
| }; | |||
| const [res] = await to(getWorkspaceAssetCountReq(params)); | |||
| if (res && res.data) { | |||
| const { component, dataset, image, model, workflow } = res.data; | |||
| const { dataset, image, model, workflow } = res.data; | |||
| const items = [ | |||
| { | |||
| title: '数据集', | |||
| @@ -30,10 +30,10 @@ function AssetsManagement() { | |||
| title: '镜像', | |||
| value: image, | |||
| }, | |||
| { | |||
| title: '组件', | |||
| value: component, | |||
| }, | |||
| // { | |||
| // title: '组件', | |||
| // value: component, | |||
| // }, | |||
| // { | |||
| // title: '代码配置', | |||
| // value: 0, | |||
| @@ -55,10 +55,11 @@ export function runActiveLearnReq(id) { | |||
| // ----------------------- 实验实例 ----------------------- | |||
| // 获取实验实例列表 | |||
| export function getActiveLearnInsListReq(params) { | |||
| export function getActiveLearnInsListReq(params, skipLoading) { | |||
| return request(`/api/mmp/activeLearnIns`, { | |||
| method: 'GET', | |||
| params, | |||
| skipLoading | |||
| }); | |||
| } | |||
| @@ -100,3 +101,12 @@ export function getExpMetricsReq(data) { | |||
| }); | |||
| } | |||
| // 编辑实验实例 | |||
| export function editActiveLearnInsReq(data) { | |||
| return request(`/api/mmp/activeLearnIns`, { | |||
| method: 'PUT', | |||
| data, | |||
| skipLoading: true, | |||
| }); | |||
| } | |||
| @@ -7,7 +7,7 @@ | |||
| import { request } from '@umijs/max'; | |||
| // 分页查询自动学习 | |||
| export function getAutoMLListReq(params,skipLoading) { | |||
| export function getAutoMLListReq(params, skipLoading) { | |||
| return request(`/api/mmp/machineLearn`, { | |||
| method: 'GET', | |||
| params, | |||
| @@ -55,10 +55,11 @@ export function runAutoMLReq(id) { | |||
| // ----------------------- 实验实例 ----------------------- | |||
| // 获取实验实例列表 | |||
| export function getExperimentInsListReq(params) { | |||
| export function getExperimentInsListReq(params, skipLoading) { | |||
| return request(`/api/mmp/machineLearnIns`, { | |||
| method: 'GET', | |||
| params, | |||
| skipLoading, | |||
| }); | |||
| } | |||
| @@ -90,3 +91,12 @@ export function batchDeleteExperimentInsReq(data) { | |||
| data, | |||
| }); | |||
| } | |||
| // 编辑实验实例 | |||
| export function editExperimentInsReq(data) { | |||
| return request(`/api/mmp/machineLearnIns`, { | |||
| method: 'PUT', | |||
| data, | |||
| skipLoading: true, | |||
| }); | |||
| } | |||
| @@ -29,10 +29,11 @@ export function deleteExperimentById(id) { | |||
| }); | |||
| } | |||
| // 根据id查询实验实例 | |||
| export function getQueryByExperimentId(params) { | |||
| export function getQueryByExperimentId(params, skipLoading) { | |||
| return request(`/api/mmp/experimentIns`, { | |||
| method: 'GET', | |||
| params, | |||
| skipLoading | |||
| }); | |||
| } | |||
| // 根据id删除实验实例 | |||
| @@ -54,6 +55,16 @@ export function putQueryByExperimentInsId(id) { | |||
| method: 'PUT', | |||
| }); | |||
| } | |||
| // 编辑实验实例 | |||
| export function editExperimentInsReq(data) { | |||
| return request(`/api/mmp/experimentIns`, { | |||
| method: 'PUT', | |||
| data, | |||
| skipLoading: true, | |||
| }); | |||
| } | |||
| // 查询实验实例实时日志 | |||
| export function getQueryByExperimentLog(data) { | |||
| return request('/api/mmp/experimentIns/realTimeLog/', { | |||
| @@ -55,10 +55,11 @@ export function runRayReq(id) { | |||
| // ----------------------- 实验实例 ----------------------- | |||
| // 获取实验实例列表 | |||
| export function getRayInsListReq(params) { | |||
| export function getRayInsListReq(params, skipLoading) { | |||
| return request(`/api/mmp/rayIns`, { | |||
| method: 'GET', | |||
| params, | |||
| skipLoading, | |||
| }); | |||
| } | |||
| @@ -98,3 +99,13 @@ export function getExpMetricsReq(data) { | |||
| data, | |||
| }); | |||
| } | |||
| // 编辑实验实例 | |||
| export function editRayInsReq(data) { | |||
| return request(`/api/mmp/rayIns`, { | |||
| method: 'PUT', | |||
| data, | |||
| skipLoading: true, | |||
| }); | |||
| } | |||
| @@ -1,4 +1,3 @@ | |||
| import { now } from '@/hooks/useServerTime'; | |||
| import dayjs from 'dayjs'; | |||
| /** | |||
| @@ -13,8 +12,12 @@ export const elapsedTime = (begin?: string | Date | null, end?: string | Date | | |||
| return '--'; | |||
| } | |||
| if (end === undefined || end === null) { | |||
| return '--'; | |||
| } | |||
| const beginDate = dayjs(begin); | |||
| const endDate = end === undefined || end === null ? dayjs(now()) : dayjs(end); | |||
| const endDate = dayjs(end); // end === undefined || end === null ? dayjs(now()) : dayjs(end); | |||
| if (!beginDate.isValid() || !endDate.isValid()) { | |||
| return '--'; | |||
| } | |||
| @@ -0,0 +1,60 @@ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { NodeStatus } from '@/types'; | |||
| import { parseJsonText } from './index'; | |||
| /** | |||
| * 获取工作流节点 | |||
| * | |||
| * @param node_status - 流水线workflow节点,json字符串 | |||
| * @return workflow 节点 | |||
| */ | |||
| export const getWorkflowStatus = (node_status?: string | null) => { | |||
| if (!node_status) { | |||
| return undefined; | |||
| } | |||
| const nodeStatusJson = parseJsonText(node_status); | |||
| if (!nodeStatusJson) { | |||
| return undefined; | |||
| } | |||
| for (const key in nodeStatusJson) { | |||
| if (key.startsWith('workflow')) { | |||
| return nodeStatusJson[key]; | |||
| } | |||
| } | |||
| return undefined; | |||
| }; | |||
| /** | |||
| * 获取实例状态 | |||
| * 终止或者 workflowStatus 不存在时,取实例状态,否则取流水线状态 | |||
| * | |||
| * @param instanceStatus - 实例状态 | |||
| * @param workflowStatus - 流水线workflow节点 | |||
| * @return 实例状态 | |||
| */ | |||
| export const getExperimentInstanceStatus = ( | |||
| instanceStatus: ExperimentStatus, | |||
| workflowStatus?: NodeStatus, | |||
| ): ExperimentStatus => { | |||
| return instanceStatus === ExperimentStatus.Terminated || !workflowStatus | |||
| ? instanceStatus | |||
| : workflowStatus?.phase; | |||
| }; | |||
| /** | |||
| * 获取实例状态 | |||
| * 终止或者 workflowStatus 不存在时,取实例状态,否则取流水线状态 | |||
| * | |||
| * @param instanceStatus - 实例状态 | |||
| * @param workflowStatus - 流水线workflow节点 | |||
| * @return 实例状态 | |||
| */ | |||
| export const getInstanceStatusInList = ( | |||
| instanceStatus: ExperimentStatus, | |||
| node_status?: string | null, | |||
| ): ExperimentStatus => { | |||
| const workflowStatus = getWorkflowStatus(node_status); | |||
| return getExperimentInstanceStatus(instanceStatus, workflowStatus); | |||
| }; | |||
| @@ -352,27 +352,3 @@ export const trimCharacter = (str: string, ch: string): string => { | |||
| export const convertEmptyStringToUndefined = (value?: string): string | undefined => { | |||
| return value === '' ? undefined : value; | |||
| }; | |||
| /** | |||
| * 获取工作流节点 | |||
| * | |||
| * @param node_status - the status of the node | |||
| * @return the workflow node | |||
| */ | |||
| export const getWorkflowStatus = (node_status?: string | null) => { | |||
| if (!node_status) { | |||
| return; | |||
| } | |||
| const nodeStatusJson = parseJsonText(node_status); | |||
| if (!nodeStatusJson) { | |||
| return; | |||
| } | |||
| for (const key in nodeStatusJson) { | |||
| if (key.startsWith('workflow')) { | |||
| return nodeStatusJson[key]; | |||
| } | |||
| } | |||
| return; | |||
| }; | |||