| @@ -11,6 +11,7 @@ import { getAccessToken } from './access'; | |||||
| import ErrorBoundary from './components/ErrorBoundary'; | import ErrorBoundary from './components/ErrorBoundary'; | ||||
| import './dayjsConfig'; | import './dayjsConfig'; | ||||
| import { removeAllPageCacheState } from './hooks/useCacheState'; | import { removeAllPageCacheState } from './hooks/useCacheState'; | ||||
| import { globalGetSeverTime } from './hooks/useServerTime'; | |||||
| import { | import { | ||||
| getRemoteMenu, | getRemoteMenu, | ||||
| getRoutersInfo, | getRoutersInfo, | ||||
| @@ -29,6 +30,7 @@ export { requestConfig as request } from './requestConfig'; | |||||
| export async function getInitialState(): Promise<GlobalInitialState> { | export async function getInitialState(): Promise<GlobalInitialState> { | ||||
| const fetchUserInfo = async () => { | const fetchUserInfo = async () => { | ||||
| try { | try { | ||||
| globalGetSeverTime(); | |||||
| const response = await getUserInfo(); | const response = await getUserInfo(); | ||||
| return { | return { | ||||
| ...response.user, | ...response.user, | ||||
| @@ -0,0 +1,35 @@ | |||||
| import { useServerTime } from '@/hooks/useServerTime'; | |||||
| import { elapsedTime } from '@/utils/date'; | |||||
| import React, { useEffect, useState } from 'react'; | |||||
| type RunDurationProps = { | |||||
| createTime?: string; | |||||
| finishTime?: string; | |||||
| className?: string; | |||||
| style?: React.CSSProperties; | |||||
| }; | |||||
| function RunDuration({ createTime, finishTime, className, style }: RunDurationProps) { | |||||
| const [now] = useServerTime(); | |||||
| const [currentTime, setCurrentTime] = useState<Date>(now()); | |||||
| // 定时刷新耗时 | |||||
| useEffect(() => { | |||||
| if (finishTime) { | |||||
| setCurrentTime(new Date(finishTime)); | |||||
| } else { | |||||
| const timer = setInterval(() => { | |||||
| setCurrentTime(now()); | |||||
| }, 1000); | |||||
| return () => { | |||||
| clearInterval(timer); | |||||
| }; | |||||
| } | |||||
| }, [finishTime, now]); | |||||
| return ( | |||||
| <span className={className} style={style}> | |||||
| {elapsedTime(createTime, currentTime)} | |||||
| </span> | |||||
| ); | |||||
| } | |||||
| export default RunDuration; | |||||
| @@ -1,11 +1,12 @@ | |||||
| import { parseJsonText } from '@/utils'; | import { parseJsonText } from '@/utils'; | ||||
| import { useCallback, useRef } from 'react'; | |||||
| import { useEffect } from 'react'; | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { NodeStatus } from '@/types'; | |||||
| export const useSSE = (onMessage: (data: any) => void) => { | |||||
| const evtSourceRef = useRef<EventSource | null>(null); | |||||
| const setupSSE = useCallback( | |||||
| (name: string, namespace: string) => { | |||||
| 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) => { | |||||
| useEffect(() => { | |||||
| if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { | |||||
| const { origin } = location; | const { origin } = location; | ||||
| const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); | const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); | ||||
| const evtSource = new EventSource( | const evtSource = new EventSource( | ||||
| @@ -18,11 +19,10 @@ export const useSSE = (onMessage: (data: any) => void) => { | |||||
| return; | return; | ||||
| } | } | ||||
| const dataJson = parseJsonText(data); | const dataJson = parseJsonText(data); | ||||
| if (dataJson) { | |||||
| const nodes = dataJson?.result?.object?.status?.nodes; | |||||
| if (nodes) { | |||||
| onMessage(nodes); | |||||
| } | |||||
| const statusData = dataJson?.result?.object?.status; | |||||
| if (statusData) { | |||||
| const { finishedAt, phase, nodes } = statusData; | |||||
| onMessage(experimentInsId, phase, finishedAt, nodes); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -30,17 +30,10 @@ export const useSSE = (onMessage: (data: any) => void) => { | |||||
| console.error('SSE error: ', error); | console.error('SSE error: ', error); | ||||
| }; | }; | ||||
| evtSourceRef.current = evtSource; | |||||
| }, | |||||
| [onMessage], | |||||
| ); | |||||
| const closeSSE = useCallback(() => { | |||||
| if (evtSourceRef.current) { | |||||
| evtSourceRef.current.close(); | |||||
| evtSourceRef.current = null; | |||||
| return () => { | |||||
| evtSource.close(); | |||||
| } | |||||
| } | } | ||||
| }, []); | |||||
| return [setupSSE, closeSSE]; | |||||
| }, [experimentInsId, status, name, namespace, onMessage]); | |||||
| }; | }; | ||||
| @@ -0,0 +1,54 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-10-10 08:51:41 | |||||
| * @Description: 服务器时间 hook | |||||
| */ | |||||
| import { getSeverTimeReq } from '@/services/experiment'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import { useCallback, useEffect, useState } from 'react'; | |||||
| let globalTimeOffset: number | undefined = undefined; | |||||
| export const globalGetSeverTime = async () => { | |||||
| const requestStartTime = Date.now() | |||||
| const [res] = await to(getSeverTimeReq()); | |||||
| const requestEndTime = Date.now() | |||||
| const requestDuration = (requestEndTime - requestStartTime) / 2; | |||||
| if (res && res.data) { | |||||
| const serverDate = new Date(res.data); | |||||
| const timeOffset = serverDate.getTime() + requestDuration - requestEndTime ; | |||||
| globalTimeOffset = timeOffset; | |||||
| return timeOffset | |||||
| } | |||||
| }; | |||||
| export const now = () => { | |||||
| return new Date(Date.now() + (globalTimeOffset ?? 0)) | |||||
| } | |||||
| /** 获取服务器时间 */ | |||||
| export function useServerTime() { | |||||
| const [timeOffset, setTimeOffset] = useState<number>(globalTimeOffset ?? 0); | |||||
| useEffect(() => { | |||||
| // 获取服务器时间 | |||||
| const getSeverTime = async () => { | |||||
| const [res] = await to(globalGetSeverTime()); | |||||
| if (res) { | |||||
| setTimeOffset(res) | |||||
| } | |||||
| }; | |||||
| if (!globalTimeOffset) { | |||||
| getSeverTime(); | |||||
| } | |||||
| }, []); | |||||
| const now = useCallback(() => { | |||||
| return new Date(Date.now() + timeOffset) | |||||
| }, [timeOffset]) | |||||
| return [now, timeOffset] as const; | |||||
| } | |||||
| @@ -106,13 +106,13 @@ function ActiveLearnInstance() { | |||||
| if (dataJson) { | if (dataJson) { | ||||
| const nodes = dataJson?.result?.object?.status?.nodes; | const nodes = dataJson?.result?.object?.status?.nodes; | ||||
| if (nodes) { | if (nodes) { | ||||
| // 节点 | |||||
| setNodes(nodes); | |||||
| const workflowStatus = Object.values(nodes).find((node: any) => | const workflowStatus = Object.values(nodes).find((node: any) => | ||||
| node.displayName.startsWith(NodePrefix), | node.displayName.startsWith(NodePrefix), | ||||
| ) as NodeStatus; | ) as NodeStatus; | ||||
| // 节点 | |||||
| setNodes(nodes); | |||||
| // 设置工作流状态 | // 设置工作流状态 | ||||
| if (workflowStatus) { | if (workflowStatus) { | ||||
| setWorkflowStatus(workflowStatus); | setWorkflowStatus(workflowStatus); | ||||
| @@ -153,6 +153,7 @@ function ActiveLearnInstance() { | |||||
| className={styles['active-learn-instance__basic']} | className={styles['active-learn-instance__basic']} | ||||
| info={experimentInfo} | info={experimentInfo} | ||||
| runStatus={workflowStatus} | runStatus={workflowStatus} | ||||
| instanceStatus={instanceInfo?.status} | |||||
| isInstance | isInstance | ||||
| /> | /> | ||||
| ), | ), | ||||
| @@ -1,5 +1,5 @@ | |||||
| import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; | import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; | ||||
| import { AutoMLTaskType, autoMLTaskTypeOptions } from '@/enums'; | |||||
| import { AutoMLTaskType, autoMLTaskTypeOptions, ExperimentStatus } from '@/enums'; | |||||
| import { useComputingResource } from '@/hooks/useComputingResource'; | import { useComputingResource } from '@/hooks/useComputingResource'; | ||||
| import { | import { | ||||
| classifierAlgorithms, | classifierAlgorithms, | ||||
| @@ -9,9 +9,8 @@ import { | |||||
| regressorAlgorithms, | regressorAlgorithms, | ||||
| } from '@/pages/ActiveLearn/components/CreateForm/utils'; | } from '@/pages/ActiveLearn/components/CreateForm/utils'; | ||||
| import { ActiveLearnData } from '@/pages/ActiveLearn/types'; | import { ActiveLearnData } from '@/pages/ActiveLearn/types'; | ||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import ExperimentRunBasic from '@/pages/AutoML/components/ExperimentRunBasic'; | |||||
| import { type NodeStatus } from '@/types'; | import { type NodeStatus } from '@/types'; | ||||
| import { elapsedTime } from '@/utils/date'; | |||||
| import { | import { | ||||
| formatBoolean, | formatBoolean, | ||||
| formatCodeConfig, | formatCodeConfig, | ||||
| @@ -21,7 +20,6 @@ import { | |||||
| formatMirror, | formatMirror, | ||||
| formatModel, | formatModel, | ||||
| } from '@/utils/format'; | } from '@/utils/format'; | ||||
| import { Flex } from 'antd'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| @@ -31,9 +29,16 @@ type BasicInfoProps = { | |||||
| className?: string; | className?: string; | ||||
| isInstance?: boolean; | isInstance?: boolean; | ||||
| runStatus?: NodeStatus; | runStatus?: NodeStatus; | ||||
| instanceStatus?: ExperimentStatus; | |||||
| }; | }; | ||||
| function BasicInfo({ info, className, runStatus, isInstance = false }: BasicInfoProps) { | |||||
| function BasicInfo({ | |||||
| info, | |||||
| className, | |||||
| runStatus, | |||||
| instanceStatus, | |||||
| isInstance = false, | |||||
| }: BasicInfoProps) { | |||||
| const getResourceDescription = useComputingResource()[1]; | const getResourceDescription = useComputingResource()[1]; | ||||
| const basicDatas: BasicInfoData[] = useMemo(() => { | const basicDatas: BasicInfoData[] = useMemo(() => { | ||||
| if (!info) { | if (!info) { | ||||
| @@ -205,56 +210,13 @@ function BasicInfo({ info, className, runStatus, isInstance = false }: BasicInfo | |||||
| ]; | ]; | ||||
| }, [info, getResourceDescription]); | }, [info, getResourceDescription]); | ||||
| const instanceDatas = useMemo(() => { | |||||
| if (!info || !runStatus) { | |||||
| return []; | |||||
| } | |||||
| return [ | |||||
| { | |||||
| label: '启动时间', | |||||
| value: formatDate(info.create_time), | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '执行时长', | |||||
| value: elapsedTime(info.create_time, runStatus.finishedAt), | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '状态', | |||||
| value: ( | |||||
| <Flex align="center"> | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '7px' }} | |||||
| src={experimentStatusInfo[runStatus.phase]?.icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| <div | |||||
| style={{ | |||||
| color: experimentStatusInfo[runStatus?.phase]?.color, | |||||
| fontSize: '15px', | |||||
| lineHeight: 1.6, | |||||
| }} | |||||
| > | |||||
| {experimentStatusInfo[runStatus?.phase]?.label} | |||||
| </div> | |||||
| </Flex> | |||||
| ), | |||||
| ellipsis: true, | |||||
| }, | |||||
| ]; | |||||
| }, [runStatus, info]); | |||||
| return ( | return ( | ||||
| <div className={classNames(styles['active-learn-basic'], className)}> | <div className={classNames(styles['active-learn-basic'], className)}> | ||||
| {isInstance && runStatus && ( | {isInstance && runStatus && ( | ||||
| <ConfigInfo | |||||
| title="运行信息" | |||||
| datas={instanceDatas} | |||||
| labelWidth={70} | |||||
| style={{ marginBottom: '20px' }} | |||||
| <ExperimentRunBasic | |||||
| create_time={info?.create_time} | |||||
| runStatus={runStatus} | |||||
| instanceStatus={instanceStatus} | |||||
| /> | /> | ||||
| )} | )} | ||||
| {!isInstance && ( | {!isInstance && ( | ||||
| @@ -110,13 +110,13 @@ function AutoMLInstance() { | |||||
| if (dataJson) { | if (dataJson) { | ||||
| const nodes = dataJson?.result?.object?.status?.nodes; | const nodes = dataJson?.result?.object?.status?.nodes; | ||||
| if (nodes) { | if (nodes) { | ||||
| // 节点 | |||||
| setNodes(nodes); | |||||
| const workflowStatus = Object.values(nodes).find((node: any) => | const workflowStatus = Object.values(nodes).find((node: any) => | ||||
| node.displayName.startsWith(NodePrefix), | node.displayName.startsWith(NodePrefix), | ||||
| ) as NodeStatus; | ) as NodeStatus; | ||||
| // 节点 | |||||
| setNodes(nodes); | |||||
| if (workflowStatus) { | if (workflowStatus) { | ||||
| setWorkflowStatus(workflowStatus); | setWorkflowStatus(workflowStatus); | ||||
| @@ -156,6 +156,7 @@ function AutoMLInstance() { | |||||
| className={styles['auto-ml-instance__basic']} | className={styles['auto-ml-instance__basic']} | ||||
| info={autoMLInfo} | info={autoMLInfo} | ||||
| runStatus={workflowStatus} | runStatus={workflowStatus} | ||||
| instanceStatus={instanceInfo?.status} | |||||
| isInstance | isInstance | ||||
| /> | /> | ||||
| ), | ), | ||||
| @@ -2,19 +2,18 @@ import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; | |||||
| import { | import { | ||||
| AutoMLTaskType, | AutoMLTaskType, | ||||
| AutoMLType, | AutoMLType, | ||||
| ExperimentStatus, | |||||
| autoMLEnsembleClassOptions, | autoMLEnsembleClassOptions, | ||||
| autoMLTaskTypeOptions, | autoMLTaskTypeOptions, | ||||
| } from '@/enums'; | } from '@/enums'; | ||||
| import { useComputingResource } from '@/hooks/useComputingResource'; | import { useComputingResource } from '@/hooks/useComputingResource'; | ||||
| import { AutoMLData } from '@/pages/AutoML/types'; | import { AutoMLData } from '@/pages/AutoML/types'; | ||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import { type NodeStatus } from '@/types'; | import { type NodeStatus } from '@/types'; | ||||
| import { parseJsonText } from '@/utils'; | import { parseJsonText } from '@/utils'; | ||||
| import { elapsedTime } from '@/utils/date'; | |||||
| import { formatBoolean, formatDataset, formatDate, formatEnum } from '@/utils/format'; | import { formatBoolean, formatDataset, formatDate, formatEnum } from '@/utils/format'; | ||||
| import { Flex } from 'antd'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||
| import ExperimentRunBasic from '../ExperimentRunBasic'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| // 格式化优化方向 | // 格式化优化方向 | ||||
| @@ -40,9 +39,16 @@ type AutoMLBasicProps = { | |||||
| className?: string; | className?: string; | ||||
| isInstance?: boolean; | isInstance?: boolean; | ||||
| runStatus?: NodeStatus; | runStatus?: NodeStatus; | ||||
| instanceStatus?: ExperimentStatus; | |||||
| }; | }; | ||||
| function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLBasicProps) { | |||||
| function AutoMLBasic({ | |||||
| info, | |||||
| className, | |||||
| runStatus, | |||||
| instanceStatus, | |||||
| isInstance = false, | |||||
| }: AutoMLBasicProps) { | |||||
| const getResourceDescription = useComputingResource()[1]; | const getResourceDescription = useComputingResource()[1]; | ||||
| const basicDatas: BasicInfoData[] = useMemo(() => { | const basicDatas: BasicInfoData[] = useMemo(() => { | ||||
| if (!info) { | if (!info) { | ||||
| @@ -284,53 +290,13 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| ]; | ]; | ||||
| }, [info]); | }, [info]); | ||||
| const instanceDatas = useMemo(() => { | |||||
| if (!info || !runStatus) { | |||||
| return []; | |||||
| } | |||||
| return [ | |||||
| { | |||||
| label: '启动时间', | |||||
| value: formatDate(info.create_time), | |||||
| }, | |||||
| { | |||||
| label: '执行时长', | |||||
| value: elapsedTime(info.create_time, runStatus.finishedAt), | |||||
| }, | |||||
| { | |||||
| label: '状态', | |||||
| value: ( | |||||
| <Flex align="center"> | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '7px' }} | |||||
| src={experimentStatusInfo[runStatus.phase]?.icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| <div | |||||
| style={{ | |||||
| color: experimentStatusInfo[runStatus?.phase]?.color, | |||||
| fontSize: '15px', | |||||
| lineHeight: 1.6, | |||||
| }} | |||||
| > | |||||
| {experimentStatusInfo[runStatus?.phase]?.label} | |||||
| </div> | |||||
| </Flex> | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| }, [runStatus, info]); | |||||
| return ( | return ( | ||||
| <div className={classNames(styles['auto-ml-basic'], className)}> | <div className={classNames(styles['auto-ml-basic'], className)}> | ||||
| {isInstance && runStatus && ( | {isInstance && runStatus && ( | ||||
| <ConfigInfo | |||||
| title="运行信息" | |||||
| datas={instanceDatas} | |||||
| labelWidth={70} | |||||
| style={{ marginBottom: '20px' }} | |||||
| <ExperimentRunBasic | |||||
| create_time={info?.create_time} | |||||
| runStatus={runStatus} | |||||
| instanceStatus={instanceStatus} | |||||
| /> | /> | ||||
| )} | )} | ||||
| {!isInstance && ( | {!isInstance && ( | ||||
| @@ -1,20 +1,19 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { ExperimentStatus } from '@/enums'; | import { ExperimentStatus } from '@/enums'; | ||||
| import { useCheck } from '@/hooks/useCheck'; | import { useCheck } from '@/hooks/useCheck'; | ||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { type ExperimentInstance } from '@/types'; | import { type ExperimentInstance } from '@/types'; | ||||
| 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, Checkbox, ConfigProvider, Typography } from 'antd'; | |||||
| import { App, Button, Checkbox, ConfigProvider } from 'antd'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useEffect, useMemo } from 'react'; | import { useEffect, useMemo } from 'react'; | ||||
| import { ExperimentListType, experimentListConfig } from '../ExperimentList/config'; | import { ExperimentListType, experimentListConfig } from '../ExperimentList/config'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import ExperimentInstanceComponent from './instance'; | |||||
| type ExperimentInstanceProps = { | |||||
| type ExperimentInstanceListProps = { | |||||
| type: ExperimentListType; | type: ExperimentListType; | ||||
| experimentInsList?: ExperimentInstance[]; | experimentInsList?: ExperimentInstance[]; | ||||
| experimentInsTotal: number; | experimentInsTotal: number; | ||||
| @@ -24,7 +23,7 @@ type ExperimentInstanceProps = { | |||||
| onLoadMore?: () => void; | onLoadMore?: () => void; | ||||
| }; | }; | ||||
| function ExperimentInstanceComponent({ | |||||
| function ExperimentInstanceList({ | |||||
| type, | type, | ||||
| experimentInsList, | experimentInsList, | ||||
| experimentInsTotal, | experimentInsTotal, | ||||
| @@ -32,7 +31,7 @@ function ExperimentInstanceComponent({ | |||||
| onRemove, | onRemove, | ||||
| onTerminate, | onTerminate, | ||||
| onLoadMore, | onLoadMore, | ||||
| }: ExperimentInstanceProps) { | |||||
| }: ExperimentInstanceListProps) { | |||||
| const { message } = App.useApp(); | const { message } = App.useApp(); | ||||
| const allIntanceIds = useMemo(() => { | const allIntanceIds = useMemo(() => { | ||||
| return experimentInsList?.map((item) => item.id) || []; | return experimentInsList?.map((item) => item.id) || []; | ||||
| @@ -171,28 +170,14 @@ function ExperimentInstanceComponent({ | |||||
| > | > | ||||
| {index + 1} | {index + 1} | ||||
| </a> | </a> | ||||
| <div className={styles.description}> | |||||
| {elapsedTime(item.create_time, item.finish_time)} | |||||
| </div> | |||||
| <div className={styles.startTime}> | |||||
| <Typography.Text ellipsis={{ tooltip: formatDate(item.create_time) }}> | |||||
| {formatDate(item.create_time)} | |||||
| </Typography.Text> | |||||
| </div> | |||||
| <div className={styles.statusBox}> | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '7px' }} | |||||
| src={experimentStatusInfo[item.status as ExperimentStatus]?.icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| <span | |||||
| style={{ color: experimentStatusInfo[item.status as ExperimentStatus]?.color }} | |||||
| className={styles.statusIcon} | |||||
| > | |||||
| {experimentStatusInfo[item.status as ExperimentStatus]?.label} | |||||
| </span> | |||||
| </div> | |||||
| <ExperimentInstanceComponent | |||||
| create_time={item.create_time} | |||||
| finish_time={item.finish_time} | |||||
| status={item.status as ExperimentStatus} | |||||
| argo_ins_name={item.argo_ins_name} | |||||
| argo_ins_ns={item.argo_ins_ns} | |||||
| experimentInsId={item.id} | |||||
| ></ExperimentInstanceComponent> | |||||
| <div className={styles.operation}> | <div className={styles.operation}> | ||||
| <Button | <Button | ||||
| type="link" | type="link" | ||||
| @@ -244,4 +229,4 @@ function ExperimentInstanceComponent({ | |||||
| ); | ); | ||||
| } | } | ||||
| export default ExperimentInstanceComponent; | |||||
| export default ExperimentInstanceList; | |||||
| @@ -0,0 +1,68 @@ | |||||
| import RunDuration from '@/components/RunDuration'; | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { useSSE, type MessageHandler } from '@/hooks/useSSE'; | |||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import { ExperimentCompleted } from '@/utils/constant'; | |||||
| import { formatDate } from '@/utils/date'; | |||||
| import { Typography } from 'antd'; | |||||
| import React, { useCallback } from 'react'; | |||||
| import styles from './index.less'; | |||||
| type ExperimentInstanceProps = { | |||||
| create_time?: string; | |||||
| finish_time?: string; | |||||
| status: ExperimentStatus; | |||||
| argo_ins_name: string; | |||||
| argo_ins_ns: string; | |||||
| experimentInsId: number; | |||||
| }; | |||||
| function ExperimentInstance({ | |||||
| create_time, | |||||
| finish_time, | |||||
| status, | |||||
| argo_ins_name, | |||||
| argo_ins_ns, | |||||
| experimentInsId, | |||||
| }: ExperimentInstanceProps) { | |||||
| const handleSSEMessage: MessageHandler = useCallback( | |||||
| (experimentInsId: number, status: string, finish_time: string) => { | |||||
| window.postMessage({ | |||||
| type: ExperimentCompleted, | |||||
| payload: { | |||||
| id: experimentInsId, | |||||
| status, | |||||
| finish_time, | |||||
| }, | |||||
| }); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| useSSE(experimentInsId, status, argo_ins_name, argo_ins_ns, handleSSEMessage); | |||||
| return ( | |||||
| <React.Fragment> | |||||
| <div className={styles.description}> | |||||
| <RunDuration createTime={create_time} finishTime={finish_time} /> | |||||
| </div> | |||||
| <div className={styles.startTime}> | |||||
| <Typography.Text ellipsis={{ tooltip: formatDate(create_time) }}> | |||||
| {formatDate(create_time)} | |||||
| </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> | |||||
| </div> | |||||
| </React.Fragment> | |||||
| ); | |||||
| } | |||||
| export default ExperimentInstance; | |||||
| @@ -8,10 +8,12 @@ import KFIcon from '@/components/KFIcon'; | |||||
| import PageTitle from '@/components/PageTitle'; | import PageTitle from '@/components/PageTitle'; | ||||
| import { ExperimentStatus, autoMLTypeOptions } from '@/enums'; | import { ExperimentStatus, autoMLTypeOptions } from '@/enums'; | ||||
| import { useCacheState } from '@/hooks/useCacheState'; | import { useCacheState } from '@/hooks/useCacheState'; | ||||
| import { useServerTime } from '@/hooks/useServerTime'; | |||||
| import { AutoMLData } from '@/pages/AutoML/types'; | import { AutoMLData } from '@/pages/AutoML/types'; | ||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | import { experimentStatusInfo } from '@/pages/Experiment/status'; | ||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { type ExperimentInstance as ExperimentInstanceData } from '@/types'; | import { type ExperimentInstance as ExperimentInstanceData } from '@/types'; | ||||
| import { ExperimentCompleted } from '@/utils/constant'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | import tableCellRender, { TableCellValueType } from '@/utils/table'; | ||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| @@ -28,8 +30,8 @@ import { | |||||
| } from 'antd'; | } from 'antd'; | ||||
| import { type SearchProps } from 'antd/es/input'; | import { type SearchProps } from 'antd/es/input'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useCallback, useEffect, useState } from 'react'; | |||||
| import ExperimentInstance from '../ExperimentInstance'; | |||||
| import { useCallback, useEffect, useRef, useState } from 'react'; | |||||
| import ExperimentInstanceList from '../ExperimentInstanceList'; | |||||
| import { ExperimentListType, experimentListConfig } from './config'; | import { ExperimentListType, experimentListConfig } from './config'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| @@ -50,6 +52,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]); | const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]); | ||||
| const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]); | const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]); | ||||
| const [experimentInsTotal, setExperimentInsTotal] = useState(0); | const [experimentInsTotal, setExperimentInsTotal] = useState(0); | ||||
| const [now] = useServerTime(); | |||||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | const [pagination, setPagination] = useState<TablePaginationConfig>( | ||||
| cacheState?.pagination ?? { | cacheState?.pagination ?? { | ||||
| current: 1, | current: 1, | ||||
| @@ -57,6 +60,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| }, | }, | ||||
| ); | ); | ||||
| const config = experimentListConfig[type]; | const config = experimentListConfig[type]; | ||||
| const timerRef = useRef<ReturnType<typeof window.setTimeout> | undefined>(); | |||||
| // 获取自主机器学习或超参数自动优化列表 | // 获取自主机器学习或超参数自动优化列表 | ||||
| const getAutoMLList = useCallback(async () => { | const getAutoMLList = useCallback(async () => { | ||||
| @@ -78,6 +82,89 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| getAutoMLList(); | getAutoMLList(); | ||||
| }, [getAutoMLList]); | }, [getAutoMLList]); | ||||
| // 获取实验实例列表 | |||||
| const getExperimentInsList = useCallback( | |||||
| async (recordId: number, page: number, size: number) => { | |||||
| const params = { | |||||
| [config.idProperty]: recordId, | |||||
| page: page, | |||||
| size: size, | |||||
| }; | |||||
| const request = config.getInsListReq; | |||||
| const [res] = await to(request(params)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| try { | |||||
| if (page === 0) { | |||||
| setExperimentInsList(content); | |||||
| } else { | |||||
| setExperimentInsList((prev) => [...prev, ...content]); | |||||
| } | |||||
| setExperimentInsTotal(totalElements); | |||||
| } catch (error) { | |||||
| console.error('JSON parse error: ', error); | |||||
| } | |||||
| } | |||||
| }, | |||||
| [config.getInsListReq, config.idProperty], | |||||
| ); | |||||
| // 刷新实验列表状态, | |||||
| // TODO: 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||||
| const refreshExperimentList = useCallback(() => { | |||||
| getAutoMLList(); | |||||
| }, [getAutoMLList]); | |||||
| // 刷新实验实例列表 | |||||
| const refreshExperimentIns = useCallback( | |||||
| (experimentId: number) => { | |||||
| const length = experimentInsList.length; | |||||
| getExperimentInsList(experimentId, 0, length); | |||||
| }, | |||||
| [getExperimentInsList, experimentInsList], | |||||
| ); | |||||
| // 新增,删除版本时,重置分页,然后刷新版本列表 | |||||
| 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, | |||||
| ), | |||||
| ); | |||||
| if (timerRef.current) { | |||||
| clearTimeout(timerRef.current); | |||||
| timerRef.current = undefined; | |||||
| } | |||||
| timerRef.current = setTimeout(() => { | |||||
| refreshExperimentList(); | |||||
| }, 10000); | |||||
| } | |||||
| }; | |||||
| window.addEventListener('message', handleMessage); | |||||
| return () => { | |||||
| window.removeEventListener('message', handleMessage); | |||||
| if (timerRef.current) { | |||||
| clearTimeout(timerRef.current); | |||||
| timerRef.current = undefined; | |||||
| } | |||||
| }; | |||||
| }, [refreshExperimentList]); | |||||
| // 搜索 | // 搜索 | ||||
| const onSearch: SearchProps['onSearch'] = (value) => { | const onSearch: SearchProps['onSearch'] = (value) => { | ||||
| setSearchText(value); | setSearchText(value); | ||||
| @@ -151,40 +238,17 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| message.success('运行成功'); | message.success('运行成功'); | ||||
| setExpandedRowKeys([record.id]); | setExpandedRowKeys([record.id]); | ||||
| refreshExperimentList(); | refreshExperimentList(); | ||||
| refreshExperimentIns(record.id); | |||||
| getExperimentInsList(record.id, 0, 5); | |||||
| } | } | ||||
| }; | }; | ||||
| // --------------------------- 实验实例 --------------------------- | // --------------------------- 实验实例 --------------------------- | ||||
| // 获取实验实例列表 | |||||
| const getExperimentInsList = async (recordId: number, page: number) => { | |||||
| const params = { | |||||
| [config.idProperty]: recordId, | |||||
| page: page, | |||||
| size: 5, | |||||
| }; | |||||
| const request = config.getInsListReq; | |||||
| const [res] = await to(request(params)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| try { | |||||
| if (page === 0) { | |||||
| setExperimentInsList(content); | |||||
| } else { | |||||
| setExperimentInsList((prev) => [...prev, ...content]); | |||||
| } | |||||
| setExperimentInsTotal(totalElements); | |||||
| } catch (error) { | |||||
| console.error('JSON parse error: ', error); | |||||
| } | |||||
| } | |||||
| }; | |||||
| // 展开实例 | // 展开实例 | ||||
| const handleExpandChange = (expanded: boolean, record: AutoMLData) => { | const handleExpandChange = (expanded: boolean, record: AutoMLData) => { | ||||
| setExperimentInsList([]); | setExperimentInsList([]); | ||||
| if (expanded) { | if (expanded) { | ||||
| setExpandedRowKeys([record.id]); | setExpandedRowKeys([record.id]); | ||||
| getExperimentInsList(record.id, 0); | |||||
| getExperimentInsList(record.id, 0, 5); | |||||
| refreshExperimentList(); | refreshExperimentList(); | ||||
| } else { | } else { | ||||
| setExpandedRowKeys([]); | setExpandedRowKeys([]); | ||||
| @@ -196,16 +260,11 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| navigate(`instance/${autoML.id}/${record.id}`); | navigate(`instance/${autoML.id}/${record.id}`); | ||||
| }; | }; | ||||
| // 刷新实验实例列表 | |||||
| const refreshExperimentIns = (experimentId: number) => { | |||||
| getExperimentInsList(experimentId, 0); | |||||
| }; | |||||
| // 加载更多实验实例 | // 加载更多实验实例 | ||||
| const loadMoreExperimentIns = () => { | const loadMoreExperimentIns = () => { | ||||
| const page = Math.round(experimentInsList.length / 5); | const page = Math.round(experimentInsList.length / 5); | ||||
| const recordId = expandedRowKeys[0]; | const recordId = expandedRowKeys[0]; | ||||
| getExperimentInsList(recordId, page); | |||||
| getExperimentInsList(recordId, page, 5); | |||||
| }; | }; | ||||
| // 实验实例终止 | // 实验实例终止 | ||||
| @@ -218,19 +277,13 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| return { | return { | ||||
| ...item, | ...item, | ||||
| status: ExperimentStatus.Terminated, | status: ExperimentStatus.Terminated, | ||||
| finish_time: now().toISOString(), | |||||
| }; | }; | ||||
| } | } | ||||
| return item; | return item; | ||||
| }); | }); | ||||
| }); | }); | ||||
| }; | }; | ||||
| // 刷新实验列表状态, | |||||
| // 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||||
| const refreshExperimentList = () => { | |||||
| getAutoMLList(); | |||||
| }; | |||||
| // --------------------------- Table --------------------------- | // --------------------------- Table --------------------------- | ||||
| // 分页切换 | // 分页切换 | ||||
| const handleTableChange: TableProps<AutoMLData>['onChange'] = ( | const handleTableChange: TableProps<AutoMLData>['onChange'] = ( | ||||
| @@ -409,7 +462,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| onChange={handleTableChange} | onChange={handleTableChange} | ||||
| expandable={{ | expandable={{ | ||||
| expandedRowRender: (record) => ( | expandedRowRender: (record) => ( | ||||
| <ExperimentInstance | |||||
| <ExperimentInstanceList | |||||
| type={type} | type={type} | ||||
| experimentInsList={experimentInsList} | experimentInsList={experimentInsList} | ||||
| experimentInsTotal={experimentInsTotal} | experimentInsTotal={experimentInsTotal} | ||||
| @@ -420,7 +473,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| }} | }} | ||||
| onTerminate={handleInstanceTerminate} | onTerminate={handleInstanceTerminate} | ||||
| onLoadMore={() => loadMoreExperimentIns()} | onLoadMore={() => loadMoreExperimentIns()} | ||||
| ></ExperimentInstance> | |||||
| ></ExperimentInstanceList> | |||||
| ), | ), | ||||
| onExpand: handleExpandChange, | onExpand: handleExpandChange, | ||||
| expandedRowKeys: expandedRowKeys, | expandedRowKeys: expandedRowKeys, | ||||
| @@ -0,0 +1,70 @@ | |||||
| import ConfigInfo from '@/components/ConfigInfo'; | |||||
| import RunDuration from '@/components/RunDuration'; | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import { type NodeStatus } from '@/types'; | |||||
| import { formatDate } from '@/utils/format'; | |||||
| import { Flex } from 'antd'; | |||||
| import { useMemo } from 'react'; | |||||
| type ExperimentRunBasicProps = { | |||||
| create_time?: string; | |||||
| runStatus?: NodeStatus; | |||||
| instanceStatus?: ExperimentStatus; | |||||
| }; | |||||
| function ExperimentRunBasic({ create_time, runStatus, instanceStatus }: ExperimentRunBasicProps) { | |||||
| const instanceDatas = useMemo(() => { | |||||
| if (!runStatus) { | |||||
| return []; | |||||
| } | |||||
| const status = | |||||
| instanceStatus === ExperimentStatus.Terminated ? instanceStatus : runStatus.phase; | |||||
| const statusInfo = experimentStatusInfo[status]; | |||||
| return [ | |||||
| { | |||||
| label: '启动时间', | |||||
| value: formatDate(create_time), | |||||
| }, | |||||
| { | |||||
| label: '执行时长', | |||||
| value: <RunDuration createTime={create_time} finishTime={runStatus.finishedAt} />, | |||||
| }, | |||||
| { | |||||
| label: '状态', | |||||
| value: ( | |||||
| <Flex align="center"> | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '7px' }} | |||||
| src={statusInfo?.icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| <div | |||||
| style={{ | |||||
| color: statusInfo?.color, | |||||
| fontSize: '15px', | |||||
| lineHeight: 1.6, | |||||
| }} | |||||
| > | |||||
| {statusInfo?.label} | |||||
| </div> | |||||
| </Flex> | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| }, [runStatus, create_time, instanceStatus]); | |||||
| return ( | |||||
| <ConfigInfo | |||||
| title="运行信息" | |||||
| datas={instanceDatas} | |||||
| labelWidth={70} | |||||
| style={{ marginBottom: '20px' }} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| export default ExperimentRunBasic; | |||||
| @@ -55,9 +55,7 @@ | |||||
| } | } | ||||
| &__time { | &__time { | ||||
| display: flex; | display: flex; | ||||
| flex: 0 1 content; | |||||
| align-items: center; | align-items: center; | ||||
| width: 100%; | |||||
| color: #808080; | color: #808080; | ||||
| font-size: 13px; | font-size: 13px; | ||||
| } | } | ||||
| @@ -15,6 +15,8 @@ import ExperimentDrawer from '../components/ExperimentDrawer'; | |||||
| import ParamsModal from '../components/ViewParamsModal'; | import ParamsModal from '../components/ViewParamsModal'; | ||||
| import { experimentStatusInfo } from '../status'; | import { experimentStatusInfo } from '../status'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import { useServerTime } from '@/hooks/useServerTime'; | |||||
| import RunDuration from '@/components/RunDuration'; | |||||
| let graph = null; | let graph = null; | ||||
| @@ -27,7 +29,6 @@ function ExperimentText() { | |||||
| const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); | const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); | ||||
| const [propsDrawerOpen, openPropsDrawer, closePropsDrawer, propsDrawerOpenRef] = | const [propsDrawerOpen, openPropsDrawer, closePropsDrawer, propsDrawerOpenRef] = | ||||
| useVisible(false); | useVisible(false); | ||||
| const [currentDate, setCurrentDate] = useState(); | |||||
| const navigate = useNavigate(); | const navigate = useNavigate(); | ||||
| const evtSourceRef = useRef(); | const evtSourceRef = useRef(); | ||||
| const width = 110; | const width = 110; | ||||
| @@ -60,19 +61,6 @@ function ExperimentText() { | |||||
| }; | }; | ||||
| }, []); | }, []); | ||||
| // 定时刷新耗时 | |||||
| useEffect(() => { | |||||
| if (experimentIns && !experimentIns.finish_time) { | |||||
| const timer = setInterval(() => { | |||||
| setCurrentDate(new Date()); | |||||
| console.log('定时刷新'); | |||||
| }, 1000); | |||||
| return () => { | |||||
| clearInterval(timer); | |||||
| }; | |||||
| } | |||||
| }, [experimentIns]); | |||||
| // 获取流水线模版 | // 获取流水线模版 | ||||
| const getWorkflow = async () => { | const getWorkflow = async () => { | ||||
| const [res] = await to(getWorkflowById(locationParams.workflowId)); | const [res] = await to(getWorkflowById(locationParams.workflowId)); | ||||
| @@ -100,7 +88,6 @@ function ExperimentText() { | |||||
| if (res && res.data && workflowRef.current) { | if (res && res.data && workflowRef.current) { | ||||
| setExperimentIns(res.data); | setExperimentIns(res.data); | ||||
| const { status, nodes_status, argo_ins_ns, argo_ins_name, finish_time } = res.data; | const { status, nodes_status, argo_ins_ns, argo_ins_name, finish_time } = res.data; | ||||
| setCurrentDate(new Date(finish_time)); | |||||
| const workflowData = workflowRef.current; | const workflowData = workflowRef.current; | ||||
| const experimentStatusObjs = parseJsonText(nodes_status); | const experimentStatusObjs = parseJsonText(nodes_status); | ||||
| workflowData.nodes.forEach((item) => { | workflowData.nodes.forEach((item) => { | ||||
| @@ -489,7 +476,7 @@ function ExperimentText() { | |||||
| </div> | </div> | ||||
| <div className={styles['pipeline-container__top__info']}> | <div className={styles['pipeline-container__top__info']}> | ||||
| 执行时长: | 执行时长: | ||||
| {elapsedTime(experimentIns?.create_time, experimentIns?.finish_time)} | |||||
| <RunDuration createTime={experimentIns?.create_time} finishTime={experimentIns?.finish_time} /> | |||||
| </div> | </div> | ||||
| <div className={styles['pipeline-container__top__info']}> | <div className={styles['pipeline-container__top__info']}> | ||||
| 状态: | 状态: | ||||
| @@ -1,10 +1,11 @@ | |||||
| import RunDuration from '@/components/RunDuration'; | |||||
| import { ExperimentStatus } from '@/enums'; | import { ExperimentStatus } from '@/enums'; | ||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | import { experimentStatusInfo } from '@/pages/Experiment/status'; | ||||
| import { PipelineNodeModelSerialize } from '@/types'; | import { PipelineNodeModelSerialize } from '@/types'; | ||||
| import { elapsedTime, formatDate } from '@/utils/date'; | |||||
| import { formatDate } from '@/utils/date'; | |||||
| import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; | import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; | ||||
| import { Drawer, Tabs, Typography } from 'antd'; | import { Drawer, Tabs, Typography } from 'antd'; | ||||
| import { useEffect, useMemo, useState } from 'react'; | |||||
| import { useMemo } from 'react'; | |||||
| import ExperimentParameter from '../ExperimentParameter'; | import ExperimentParameter from '../ExperimentParameter'; | ||||
| import ExperimentResult from '../ExperimentResult'; | import ExperimentResult from '../ExperimentResult'; | ||||
| import LogList from '../LogList'; | import LogList from '../LogList'; | ||||
| @@ -41,22 +42,6 @@ const ExperimentDrawer = ({ | |||||
| instanceNodeStartTime, | instanceNodeStartTime, | ||||
| instanceNodeEndTime, | instanceNodeEndTime, | ||||
| }: ExperimentDrawerProps) => { | }: ExperimentDrawerProps) => { | ||||
| const [currentDate, setCurrentDate] = useState( | |||||
| instanceNodeEndTime ? new Date(instanceNodeEndTime) : new Date(), | |||||
| ); | |||||
| // 定时刷新耗时 | |||||
| useEffect(() => { | |||||
| if (!instanceNodeEndTime) { | |||||
| const timer = setInterval(() => { | |||||
| setCurrentDate(new Date()); | |||||
| }, 1000); | |||||
| return () => { | |||||
| clearInterval(timer); | |||||
| }; | |||||
| } | |||||
| }, [instanceNodeEndTime]); | |||||
| // 如果性能有问题,可以进一步拆解 | // 如果性能有问题,可以进一步拆解 | ||||
| const items = useMemo( | const items = useMemo( | ||||
| () => [ | () => [ | ||||
| @@ -158,7 +143,7 @@ const ExperimentDrawer = ({ | |||||
| </div> | </div> | ||||
| <div className={styles['experiment-drawer__info']}> | <div className={styles['experiment-drawer__info']}> | ||||
| 耗时: | 耗时: | ||||
| {elapsedTime(instanceNodeStartTime, currentDate)} | |||||
| <RunDuration createTime={instanceNodeStartTime} finishTime={instanceNodeEndTime} /> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <Tabs | <Tabs | ||||
| @@ -1,7 +1,6 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { ExperimentStatus } from '@/enums'; | import { ExperimentStatus } from '@/enums'; | ||||
| import { useCheck } from '@/hooks/useCheck'; | import { useCheck } from '@/hooks/useCheck'; | ||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import { | import { | ||||
| deleteManyExperimentIns, | deleteManyExperimentIns, | ||||
| deleteQueryByExperimentInsId, | deleteQueryByExperimentInsId, | ||||
| @@ -9,17 +8,17 @@ import { | |||||
| } from '@/services/experiment/index.js'; | } from '@/services/experiment/index.js'; | ||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { type ExperimentInstance } from '@/types'; | import { type ExperimentInstance } from '@/types'; | ||||
| 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, Checkbox, ConfigProvider, Typography } from 'antd'; | |||||
| import { App, Button, Checkbox, ConfigProvider } from 'antd'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useEffect, useMemo } from 'react'; | import { useEffect, useMemo } from 'react'; | ||||
| import TensorBoardStatusCell from '../TensorBoardStatus'; | import TensorBoardStatusCell from '../TensorBoardStatus'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import ExperimentInstanceComponent from './instance'; | |||||
| type ExperimentInstanceProps = { | |||||
| type ExperimentInstanceListProps = { | |||||
| experimentInsList?: ExperimentInstance[]; | experimentInsList?: ExperimentInstance[]; | ||||
| experimentInsTotal: number; | experimentInsTotal: number; | ||||
| onClickInstance?: (instance: ExperimentInstance) => void; | onClickInstance?: (instance: ExperimentInstance) => void; | ||||
| @@ -29,7 +28,7 @@ type ExperimentInstanceProps = { | |||||
| onLoadMore?: () => void; | onLoadMore?: () => void; | ||||
| }; | }; | ||||
| function ExperimentInstanceComponent({ | |||||
| function ExperimentInstanceList({ | |||||
| experimentInsList, | experimentInsList, | ||||
| experimentInsTotal, | experimentInsTotal, | ||||
| onClickInstance, | onClickInstance, | ||||
| @@ -37,7 +36,7 @@ function ExperimentInstanceComponent({ | |||||
| onRemove, | onRemove, | ||||
| onTerminate, | onTerminate, | ||||
| onLoadMore, | onLoadMore, | ||||
| }: ExperimentInstanceProps) { | |||||
| }: ExperimentInstanceListProps) { | |||||
| const { message } = App.useApp(); | const { message } = App.useApp(); | ||||
| const allIntanceIds = useMemo(() => { | const allIntanceIds = useMemo(() => { | ||||
| return experimentInsList?.map((item) => item.id) || []; | return experimentInsList?.map((item) => item.id) || []; | ||||
| @@ -185,28 +184,16 @@ function ExperimentInstanceComponent({ | |||||
| '--' | '--' | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| <div className={styles.description}> | |||||
| <div style={{ width: '50%' }}>{elapsedTime(item.create_time, item.finish_time)}</div> | |||||
| <div style={{ width: '50%' }} className={styles.startTime}> | |||||
| <Typography.Text ellipsis={{ tooltip: formatDate(item.create_time) }}> | |||||
| {formatDate(item.create_time)} | |||||
| </Typography.Text> | |||||
| </div> | |||||
| </div> | |||||
| <div className={styles.statusBox}> | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '7px' }} | |||||
| src={experimentStatusInfo[item.status as ExperimentStatus]?.icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| <span | |||||
| style={{ color: experimentStatusInfo[item.status as ExperimentStatus]?.color }} | |||||
| className={styles.statusIcon} | |||||
| > | |||||
| {experimentStatusInfo[item.status as ExperimentStatus]?.label} | |||||
| </span> | |||||
| </div> | |||||
| <ExperimentInstanceComponent | |||||
| create_time={item.create_time} | |||||
| finish_time={item.finish_time} | |||||
| status={item.status as ExperimentStatus} | |||||
| argo_ins_name={item.argo_ins_name} | |||||
| argo_ins_ns={item.argo_ins_ns} | |||||
| experimentInsId={item.id} | |||||
| ></ExperimentInstanceComponent> | |||||
| <div className={styles.operation}> | <div className={styles.operation}> | ||||
| <Button | <Button | ||||
| type="link" | type="link" | ||||
| @@ -258,4 +245,4 @@ function ExperimentInstanceComponent({ | |||||
| ); | ); | ||||
| } | } | ||||
| export default ExperimentInstanceComponent; | |||||
| export default ExperimentInstanceList; | |||||
| @@ -0,0 +1,70 @@ | |||||
| import RunDuration from '@/components/RunDuration'; | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { useSSE, type MessageHandler } from '@/hooks/useSSE'; | |||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import { ExperimentCompleted } from '@/utils/constant'; | |||||
| import { formatDate } from '@/utils/date'; | |||||
| import { Typography } from 'antd'; | |||||
| import React, { useCallback } from 'react'; | |||||
| import styles from './index.less'; | |||||
| type ExperimentInstanceProps = { | |||||
| create_time?: string; | |||||
| finish_time?: string; | |||||
| status: ExperimentStatus; | |||||
| argo_ins_name: string; | |||||
| argo_ins_ns: string; | |||||
| experimentInsId: number; | |||||
| }; | |||||
| function ExperimentInstance({ | |||||
| create_time, | |||||
| finish_time, | |||||
| status, | |||||
| argo_ins_name, | |||||
| argo_ins_ns, | |||||
| experimentInsId, | |||||
| }: ExperimentInstanceProps) { | |||||
| const handleSSEMessage: MessageHandler = useCallback( | |||||
| (experimentInsId: number, status: string, finish_time: string) => { | |||||
| window.postMessage({ | |||||
| type: ExperimentCompleted, | |||||
| payload: { | |||||
| id: experimentInsId, | |||||
| status, | |||||
| finish_time, | |||||
| }, | |||||
| }); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| useSSE(experimentInsId, status, argo_ins_name, argo_ins_ns, handleSSEMessage); | |||||
| return ( | |||||
| <React.Fragment> | |||||
| <div className={styles.description}> | |||||
| <div style={{ width: '50%' }}> | |||||
| <RunDuration createTime={create_time} finishTime={finish_time} /> | |||||
| </div> | |||||
| <div style={{ width: '50%' }} className={styles.startTime}> | |||||
| <Typography.Text ellipsis={{ tooltip: formatDate(create_time) }}> | |||||
| {formatDate(create_time)} | |||||
| </Typography.Text> | |||||
| </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> | |||||
| </div> | |||||
| </React.Fragment> | |||||
| ); | |||||
| } | |||||
| export default ExperimentInstance; | |||||
| @@ -15,18 +15,20 @@ import { | |||||
| } from '@/services/experiment/index.js'; | } from '@/services/experiment/index.js'; | ||||
| import { getWorkflow } from '@/services/pipeline/index.js'; | import { getWorkflow } from '@/services/pipeline/index.js'; | ||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { ExperimentCompleted } from '@/utils/constant'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | import tableCellRender, { TableCellValueType } from '@/utils/table'; | ||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| import { App, Button, ConfigProvider, Dropdown, Input, Space, Table, Tooltip } from 'antd'; | import { App, Button, ConfigProvider, Dropdown, Input, Space, Table, Tooltip } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useCallback, useEffect, useState } from 'react'; | |||||
| import { useCallback, useEffect, useRef, useState } from 'react'; | |||||
| import { useNavigate } from 'react-router-dom'; | import { useNavigate } from 'react-router-dom'; | ||||
| import { ComparisonType } from './Comparison/config'; | import { ComparisonType } from './Comparison/config'; | ||||
| import AddExperimentModal from './components/AddExperimentModal'; | import AddExperimentModal from './components/AddExperimentModal'; | ||||
| import ExperimentInstance from './components/ExperimentInstance'; | |||||
| import ExperimentInstanceList from './components/ExperimentInstanceList'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import { experimentStatusInfo } from './status'; | import { experimentStatusInfo } from './status'; | ||||
| import { useServerTime } from '@/hooks/useServerTime'; | |||||
| // 定时器 | // 定时器 | ||||
| const timerIds = new Map(); | const timerIds = new Map(); | ||||
| @@ -36,7 +38,7 @@ function Experiment() { | |||||
| const [experimentList, setExperimentList] = useState([]); | const [experimentList, setExperimentList] = useState([]); | ||||
| const [workflowList, setWorkflowList] = useState([]); | const [workflowList, setWorkflowList] = useState([]); | ||||
| const [experimentId, setExperimentId] = useState(null); | const [experimentId, setExperimentId] = useState(null); | ||||
| const [experimentInList, setExperimentInList] = useState([]); | |||||
| const [experimentInsList, setExperimentInsList] = useState([]); | |||||
| const [expandedRowKeys, setExpandedRowKeys] = useState(null); | const [expandedRowKeys, setExpandedRowKeys] = useState(null); | ||||
| const [total, setTotal] = useState(0); | const [total, setTotal] = useState(0); | ||||
| const [isAdd, setIsAdd] = useState(true); | const [isAdd, setIsAdd] = useState(true); | ||||
| @@ -46,6 +48,7 @@ function Experiment() { | |||||
| const [cacheState, setCacheState] = useCacheState(); | const [cacheState, setCacheState] = useCacheState(); | ||||
| const [searchText, setSearchText] = useState(cacheState?.searchText); | const [searchText, setSearchText] = useState(cacheState?.searchText); | ||||
| const [inputText, setInputText] = useState(cacheState?.searchText); | const [inputText, setInputText] = useState(cacheState?.searchText); | ||||
| const [now] = useServerTime(); | |||||
| const [pagination, setPagination] = useState( | const [pagination, setPagination] = useState( | ||||
| cacheState?.pagination ?? { | cacheState?.pagination ?? { | ||||
| current: 1, | current: 1, | ||||
| @@ -53,7 +56,34 @@ function Experiment() { | |||||
| }, | }, | ||||
| ); | ); | ||||
| const { message } = App.useApp(); | const { message } = App.useApp(); | ||||
| const timerRef = useRef(); | |||||
| // 获取实验列表 | |||||
| const getExperimentList = useCallback(async () => { | |||||
| const params = { | |||||
| page: pagination.current - 1, | |||||
| size: pagination.pageSize, | |||||
| name: searchText || undefined, | |||||
| }; | |||||
| const [res] = await to(getExperiment(params)); | |||||
| if (res && res.data && Array.isArray(res.data.content)) { | |||||
| setExperimentList( | |||||
| res.data.content.map((item) => { | |||||
| return { ...item, key: item.id }; | |||||
| }), | |||||
| ); | |||||
| setTotal(res.data.totalElements); | |||||
| } | |||||
| }, [pagination, searchText]); | |||||
| // 刷新实验列表状态, | |||||
| // 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||||
| const refreshExperimentList = useCallback(() => { | |||||
| getExperimentList(); | |||||
| }, [getExperimentList]); | |||||
| // 获取流水线列表 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| // 获取流水线列表 | // 获取流水线列表 | ||||
| const getWorkflowList = async () => { | const getWorkflowList = async () => { | ||||
| @@ -76,28 +106,51 @@ function Experiment() { | |||||
| }, []); | }, []); | ||||
| // 获取实验列表 | // 获取实验列表 | ||||
| const getExperimentList = useCallback(async () => { | |||||
| const params = { | |||||
| page: pagination.current - 1, | |||||
| size: pagination.pageSize, | |||||
| name: searchText || undefined, | |||||
| }; | |||||
| const [res] = await to(getExperiment(params)); | |||||
| if (res && res.data && Array.isArray(res.data.content)) { | |||||
| setExperimentList( | |||||
| res.data.content.map((item) => { | |||||
| return { ...item, key: item.id }; | |||||
| }), | |||||
| ); | |||||
| setTotal(res.data.totalElements); | |||||
| } | |||||
| }, [pagination, searchText]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| getExperimentList(); | getExperimentList(); | ||||
| }, [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, | |||||
| ), | |||||
| ); | |||||
| if (timerRef.current) { | |||||
| clearTimeout(timerRef.current); | |||||
| timerRef.current = undefined; | |||||
| } | |||||
| timerRef.current = setTimeout(() => { | |||||
| refreshExperimentList(); | |||||
| }, 10000); | |||||
| } | |||||
| }; | |||||
| window.addEventListener('message', handleMessage); | |||||
| return () => { | |||||
| window.removeEventListener('message', handleMessage); | |||||
| if (timerRef.current) { | |||||
| clearTimeout(timerRef.current); | |||||
| timerRef.current = undefined; | |||||
| } | |||||
| }; | |||||
| }, [refreshExperimentList]); | |||||
| // 搜索 | // 搜索 | ||||
| const onSearch = (value) => { | const onSearch = (value) => { | ||||
| setSearchText(value); | setSearchText(value); | ||||
| @@ -108,11 +161,11 @@ function Experiment() { | |||||
| }; | }; | ||||
| // 获取实验实例列表 | // 获取实验实例列表 | ||||
| const getQueryByExperiment = async (experimentId, page) => { | |||||
| const getQueryByExperiment = async (experimentId, page, size = 5) => { | |||||
| const params = { | const params = { | ||||
| experimentId: experimentId, | experimentId: experimentId, | ||||
| page: page, | page: page, | ||||
| size: 5, | |||||
| size: size, | |||||
| }; | }; | ||||
| const [res, error] = await to(getQueryByExperimentId(params)); | const [res, error] = await to(getQueryByExperimentId(params)); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| @@ -127,10 +180,10 @@ function Experiment() { | |||||
| }; | }; | ||||
| }); | }); | ||||
| if (page === 0) { | if (page === 0) { | ||||
| setExperimentInList(list); | |||||
| setExperimentInsList(list); | |||||
| clearExperimentInTimers(); | clearExperimentInTimers(); | ||||
| } else { | } else { | ||||
| setExperimentInList((prev) => [...prev, ...list]); | |||||
| setExperimentInsList((prev) => [...prev, ...list]); | |||||
| } | } | ||||
| setExperimentInsTotal(totalElements); | setExperimentInsTotal(totalElements); | ||||
| // 获取 TensorBoard 状态 | // 获取 TensorBoard 状态 | ||||
| @@ -173,7 +226,7 @@ function Experiment() { | |||||
| }; | }; | ||||
| const [res] = await to(getTensorBoardStatusReq(params)); | const [res] = await to(getTensorBoardStatusReq(params)); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| setExperimentInList((prevList) => { | |||||
| setExperimentInsList((prevList) => { | |||||
| return prevList.map((item) => { | return prevList.map((item) => { | ||||
| if (item.id === experimentIn.id) { | if (item.id === experimentIn.id) { | ||||
| return { | return { | ||||
| @@ -201,11 +254,11 @@ function Experiment() { | |||||
| // 展开实例 | // 展开实例 | ||||
| const expandChange = (e, record) => { | const expandChange = (e, record) => { | ||||
| clearExperimentInTimers(); | clearExperimentInTimers(); | ||||
| setExperimentInList([]); | |||||
| setExperimentInsList([]); | |||||
| if (record.id === expandedRowKeys) { | if (record.id === expandedRowKeys) { | ||||
| setExpandedRowKeys(null); | setExpandedRowKeys(null); | ||||
| } else { | } else { | ||||
| getQueryByExperiment(record.id, 0); | |||||
| getQueryByExperiment(record.id, 0, 5); | |||||
| refreshExperimentList(); | refreshExperimentList(); | ||||
| } | } | ||||
| }; | }; | ||||
| @@ -285,7 +338,7 @@ function Experiment() { | |||||
| if (res) { | if (res) { | ||||
| message.success('运行成功'); | message.success('运行成功'); | ||||
| refreshExperimentList(); | refreshExperimentList(); | ||||
| refreshExperimentIns(id); | |||||
| getQueryByExperiment(id, 0, 5); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -323,22 +376,17 @@ function Experiment() { | |||||
| } | } | ||||
| }; | }; | ||||
| // 刷新实验列表状态, | |||||
| // 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||||
| const refreshExperimentList = () => { | |||||
| getExperimentList(); | |||||
| }; | |||||
| // 实验实例终止 | // 实验实例终止 | ||||
| const handleInstanceTerminate = async (experimentIn) => { | const handleInstanceTerminate = async (experimentIn) => { | ||||
| // 刷新实验列表 | // 刷新实验列表 | ||||
| refreshExperimentList(); | refreshExperimentList(); | ||||
| setExperimentInList((prevList) => { | |||||
| setExperimentInsList((prevList) => { | |||||
| return prevList.map((item) => { | return prevList.map((item) => { | ||||
| if (item.id === experimentIn.id) { | if (item.id === experimentIn.id) { | ||||
| return { | return { | ||||
| ...item, | ...item, | ||||
| status: ExperimentStatus.Terminated, | status: ExperimentStatus.Terminated, | ||||
| finish_time: now().toISOString(), | |||||
| }; | }; | ||||
| } | } | ||||
| return item; | return item; | ||||
| @@ -367,13 +415,14 @@ function Experiment() { | |||||
| // 刷新实验实例列表 | // 刷新实验实例列表 | ||||
| const refreshExperimentIns = (experimentId) => { | const refreshExperimentIns = (experimentId) => { | ||||
| getQueryByExperiment(experimentId, 0); | |||||
| const length = experimentInsList.length; | |||||
| getQueryByExperiment(experimentId, 0, length); | |||||
| }; | }; | ||||
| // 加载更多实验实例 | // 加载更多实验实例 | ||||
| const loadMoreExperimentIns = () => { | const loadMoreExperimentIns = () => { | ||||
| const page = Math.round(experimentInList.length / 5); | |||||
| getQueryByExperiment(expandedRowKeys, page); | |||||
| const page = Math.round(experimentInsList.length / 5); | |||||
| getQueryByExperiment(expandedRowKeys, page, 5); | |||||
| }; | }; | ||||
| // 处理删除 | // 处理删除 | ||||
| @@ -544,8 +593,8 @@ function Experiment() { | |||||
| scroll={{ y: 'calc(100% - 55px)' }} | scroll={{ y: 'calc(100% - 55px)' }} | ||||
| expandable={{ | expandable={{ | ||||
| expandedRowRender: (record) => ( | expandedRowRender: (record) => ( | ||||
| <ExperimentInstance | |||||
| experimentInsList={experimentInList} | |||||
| <ExperimentInstanceList | |||||
| experimentInsList={experimentInsList} | |||||
| experimentInsTotal={experimentInsTotal} | experimentInsTotal={experimentInsTotal} | ||||
| onClickInstance={(item) => gotoInstanceInfo(item, record)} | onClickInstance={(item) => gotoInstanceInfo(item, record)} | ||||
| onClickTensorBoard={handleTensorboard} | onClickTensorBoard={handleTensorboard} | ||||
| @@ -555,7 +604,7 @@ function Experiment() { | |||||
| }} | }} | ||||
| onTerminate={handleInstanceTerminate} | onTerminate={handleInstanceTerminate} | ||||
| onLoadMore={() => loadMoreExperimentIns()} | onLoadMore={() => loadMoreExperimentIns()} | ||||
| ></ExperimentInstance> | |||||
| ></ExperimentInstanceList> | |||||
| ), | ), | ||||
| onExpand: expandChange, | onExpand: expandChange, | ||||
| expandedRowKeys: [expandedRowKeys], | expandedRowKeys: [expandedRowKeys], | ||||
| @@ -53,7 +53,7 @@ function HyperParameterInstance() { | |||||
| const info = res.data as HyperParameterInstanceData; | const info = res.data as HyperParameterInstanceData; | ||||
| const { param, node_status, argo_ins_name, argo_ins_ns, status, create_time } = info; | const { param, node_status, argo_ins_name, argo_ins_ns, status, create_time } = info; | ||||
| // 解析配置参数 | // 解析配置参数 | ||||
| const paramJson = parseJsonText(param); | |||||
| const paramJson = parseJsonText(param).data; | |||||
| if (paramJson) { | if (paramJson) { | ||||
| // 实例详情返回的参数是字符串,需要转换 | // 实例详情返回的参数是字符串,需要转换 | ||||
| if (typeof paramJson.parameters === 'string') { | if (typeof paramJson.parameters === 'string') { | ||||
| @@ -121,13 +121,13 @@ function HyperParameterInstance() { | |||||
| if (dataJson) { | if (dataJson) { | ||||
| const nodes = dataJson?.result?.object?.status?.nodes; | const nodes = dataJson?.result?.object?.status?.nodes; | ||||
| if (nodes) { | if (nodes) { | ||||
| // 节点 | |||||
| setNodes(nodes); | |||||
| const workflowStatus = Object.values(nodes).find((node: any) => | const workflowStatus = Object.values(nodes).find((node: any) => | ||||
| node.displayName.startsWith(NodePrefix), | node.displayName.startsWith(NodePrefix), | ||||
| ) as NodeStatus; | ) as NodeStatus; | ||||
| // 节点 | |||||
| setNodes(nodes); | |||||
| // 设置工作流状态 | // 设置工作流状态 | ||||
| if (workflowStatus) { | if (workflowStatus) { | ||||
| setWorkflowStatus(workflowStatus); | setWorkflowStatus(workflowStatus); | ||||
| @@ -168,6 +168,7 @@ function HyperParameterInstance() { | |||||
| className={styles['hyper-parameter-instance__basic']} | className={styles['hyper-parameter-instance__basic']} | ||||
| info={experimentInfo} | info={experimentInfo} | ||||
| runStatus={workflowStatus} | runStatus={workflowStatus} | ||||
| instanceStatus={instanceInfo?.status} | |||||
| isInstance | isInstance | ||||
| /> | /> | ||||
| ), | ), | ||||
| @@ -1,6 +1,8 @@ | |||||
| import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; | import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; | ||||
| import { hyperParameterOptimizedMode } from '@/enums'; | |||||
| import RunDuration from '@/components/RunDuration'; | |||||
| import { ExperimentStatus, hyperParameterOptimizedMode } from '@/enums'; | |||||
| import { useComputingResource } from '@/hooks/useComputingResource'; | import { useComputingResource } from '@/hooks/useComputingResource'; | ||||
| import ExperimentRunBasic from '@/pages/AutoML/components/ExperimentRunBasic'; | |||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | import { experimentStatusInfo } from '@/pages/Experiment/status'; | ||||
| import { | import { | ||||
| schedulerAlgorithms, | schedulerAlgorithms, | ||||
| @@ -8,7 +10,6 @@ import { | |||||
| } from '@/pages/HyperParameter/components/CreateForm/utils'; | } from '@/pages/HyperParameter/components/CreateForm/utils'; | ||||
| import { HyperParameterData } from '@/pages/HyperParameter/types'; | import { HyperParameterData } from '@/pages/HyperParameter/types'; | ||||
| import { type NodeStatus } from '@/types'; | import { type NodeStatus } from '@/types'; | ||||
| import { elapsedTime } from '@/utils/date'; | |||||
| import { | import { | ||||
| formatCodeConfig, | formatCodeConfig, | ||||
| formatDataset, | formatDataset, | ||||
| @@ -33,12 +34,14 @@ type HyperParameterBasicProps = { | |||||
| className?: string; | className?: string; | ||||
| isInstance?: boolean; | isInstance?: boolean; | ||||
| runStatus?: NodeStatus; | runStatus?: NodeStatus; | ||||
| instanceStatus?: ExperimentStatus; | |||||
| }; | }; | ||||
| function HyperParameterBasic({ | function HyperParameterBasic({ | ||||
| info, | info, | ||||
| className, | className, | ||||
| runStatus, | runStatus, | ||||
| instanceStatus, | |||||
| isInstance = false, | isInstance = false, | ||||
| }: HyperParameterBasicProps) { | }: HyperParameterBasicProps) { | ||||
| const getResourceDescription = useComputingResource()[1]; | const getResourceDescription = useComputingResource()[1]; | ||||
| @@ -155,7 +158,7 @@ function HyperParameterBasic({ | |||||
| }, | }, | ||||
| { | { | ||||
| label: '执行时长', | label: '执行时长', | ||||
| value: elapsedTime(info.create_time, runStatus.finishedAt), | |||||
| value: <RunDuration createTime={info.create_time} finishTime={runStatus.finishedAt} />, | |||||
| ellipsis: true, | ellipsis: true, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -187,11 +190,10 @@ function HyperParameterBasic({ | |||||
| return ( | return ( | ||||
| <div className={classNames(styles['hyper-parameter-basic'], className)}> | <div className={classNames(styles['hyper-parameter-basic'], className)}> | ||||
| {isInstance && runStatus && ( | {isInstance && runStatus && ( | ||||
| <ConfigInfo | |||||
| title="运行信息" | |||||
| datas={instanceDatas} | |||||
| labelWidth={70} | |||||
| style={{ marginBottom: '20px' }} | |||||
| <ExperimentRunBasic | |||||
| create_time={info?.create_time} | |||||
| runStatus={runStatus} | |||||
| instanceStatus={instanceStatus} | |||||
| /> | /> | ||||
| )} | )} | ||||
| {!isInstance && ( | {!isInstance && ( | ||||
| @@ -11,6 +11,7 @@ | |||||
| flex: 1; | flex: 1; | ||||
| flex-direction: column; | flex-direction: column; | ||||
| align-items: center; | align-items: center; | ||||
| padding: 0 20px; | |||||
| &--border { | &--border { | ||||
| border-right: 1px solid @border-color; | border-right: 1px solid @border-color; | ||||
| @@ -1,3 +1,5 @@ | |||||
| import { formatNumber } from '@/utils/format'; | |||||
| import { Typography } from 'antd'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| @@ -10,11 +12,11 @@ function Statistics({ remaining, consuming }: StatisticsProps) { | |||||
| const items = [ | const items = [ | ||||
| { | { | ||||
| title: '当前可用算力积分(分)', | title: '当前可用算力积分(分)', | ||||
| value: remaining ?? '-', | |||||
| value: remaining, | |||||
| }, | }, | ||||
| { | { | ||||
| title: '总消耗算力积分(分)', | title: '总消耗算力积分(分)', | ||||
| value: consuming ?? '-', | |||||
| value: consuming, | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -27,7 +29,12 @@ function Statistics({ remaining, consuming }: StatisticsProps) { | |||||
| [styles['statistics__item--border']]: index === 0, | [styles['statistics__item--border']]: index === 0, | ||||
| })} | })} | ||||
| > | > | ||||
| <span className={styles['statistics__item__value']}>{item.value}</span> | |||||
| <Typography.Paragraph | |||||
| ellipsis={{ tooltip: formatNumber(item.value) }} | |||||
| className={styles['statistics__item__value']} | |||||
| > | |||||
| {formatNumber(item.value)} | |||||
| </Typography.Paragraph> | |||||
| <span className={styles['statistics__item__title']}>{item.title}</span> | <span className={styles['statistics__item__title']}>{item.title}</span> | ||||
| </div> | </div> | ||||
| ))} | ))} | ||||
| @@ -1,5 +1,6 @@ | |||||
| import { PointsStatistics } from '@/pages/Points/index'; | import { PointsStatistics } from '@/pages/Points/index'; | ||||
| import { getPointsStatisticsReq } from '@/services/points'; | import { getPointsStatisticsReq } from '@/services/points'; | ||||
| import { formatNumber } from '@/utils/format'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { useNavigate } from '@umijs/max'; | import { useNavigate } from '@umijs/max'; | ||||
| import { Typography } from 'antd'; | import { Typography } from 'antd'; | ||||
| @@ -22,14 +23,16 @@ function UserPoints() { | |||||
| getPointsStatistics(); | getPointsStatistics(); | ||||
| }, []); | }, []); | ||||
| const userCredit = formatNumber(statistics?.userCredit); | |||||
| return ( | return ( | ||||
| <div className={styles['user-points']}> | <div className={styles['user-points']}> | ||||
| <div className={styles['user-points__label']}>当前可用算力积分</div> | <div className={styles['user-points__label']}>当前可用算力积分</div> | ||||
| <Typography.Paragraph | <Typography.Paragraph | ||||
| className={styles['user-points__value']} | className={styles['user-points__value']} | ||||
| ellipsis={{ tooltip: statistics?.userCredit ?? '--' }} | |||||
| ellipsis={{ tooltip: userCredit }} | |||||
| > | > | ||||
| {statistics?.userCredit ?? '--'} | |||||
| {userCredit} | |||||
| </Typography.Paragraph> | </Typography.Paragraph> | ||||
| <div | <div | ||||
| className={styles['user-points__button']} | className={styles['user-points__button']} | ||||
| @@ -151,3 +151,11 @@ export function getExpMetricsReq(data) { | |||||
| data, | data, | ||||
| }); | }); | ||||
| } | } | ||||
| // 获取服务器的当前时间 | |||||
| export function getSeverTimeReq(data) { | |||||
| return request(`/api/mmp/experimentIns/time`, { | |||||
| method: 'GET', | |||||
| }); | |||||
| } | |||||
| @@ -17,3 +17,6 @@ export const VersionChangedMessage = 'versionChanged'; | |||||
| // 创建服务成功消息,去创建服务版本 | // 创建服务成功消息,去创建服务版本 | ||||
| export const ServiceCreatedMessage = 'serviceCreated'; | export const ServiceCreatedMessage = 'serviceCreated'; | ||||
| // 实验完成 | |||||
| export const ExperimentCompleted = 'ExperimentCompleted'; | |||||
| @@ -193,3 +193,19 @@ export const formatEnum = (options: EnumOptions[]): FormatEnumFunc => { | |||||
| return option && option.label ? option.label : '--'; | return option && option.label ? option.label : '--'; | ||||
| }; | }; | ||||
| }; | }; | ||||
| /** | |||||
| * 格式化数字 | |||||
| * | |||||
| * @param value - 值、 | |||||
| * @param toFixed - 保留几位小数 | |||||
| * @return 格式化的数字,如果不是数字,返回 '--' | |||||
| */ | |||||
| export const formatNumber = (value?: number | null, toFixed?: number) : number | string => { | |||||
| if (typeof value !== "number") { | |||||
| return '--' | |||||
| } | |||||
| return toFixed ? Number(value).toFixed(toFixed) : value | |||||
| } | |||||
| @@ -6,6 +6,7 @@ | |||||
| import { PageEnum } from '@/enums/pagesEnums'; | import { PageEnum } from '@/enums/pagesEnums'; | ||||
| import G6 from '@antv/g6'; | import G6 from '@antv/g6'; | ||||
| import { number } from 'echarts'; | |||||
| /** | /** | ||||
| * 生成 8 位随机数 | * 生成 8 位随机数 | ||||
| @@ -346,3 +347,4 @@ export const trimCharacter = (str: string, ch: string): string => { | |||||
| export const convertEmptyStringToUndefined = (value?: string): string | undefined => { | export const convertEmptyStringToUndefined = (value?: string): string | undefined => { | ||||
| return value === '' ? undefined : value; | return value === '' ? undefined : value; | ||||
| }; | }; | ||||