Browse Source

fix: 实验运行时长定时刷新

pull/216/head
zhaowei 9 months ago
parent
commit
bef5bf649e
23 changed files with 503 additions and 203 deletions
  1. +2
    -0
      react-ui/src/app.tsx
  2. +16
    -23
      react-ui/src/hooks/useSSE.ts
  3. +54
    -0
      react-ui/src/hooks/useServerTime.ts
  4. +8
    -6
      react-ui/src/pages/ActiveLearn/Instance/index.tsx
  5. +8
    -6
      react-ui/src/pages/AutoML/Instance/index.tsx
  6. +0
    -0
      react-ui/src/pages/AutoML/components/ExperimentInstanceList/index.less
  7. +14
    -29
      react-ui/src/pages/AutoML/components/ExperimentInstanceList/index.tsx
  8. +83
    -0
      react-ui/src/pages/AutoML/components/ExperimentInstanceList/instance.tsx
  9. +80
    -41
      react-ui/src/pages/AutoML/components/ExperimentList/index.tsx
  10. +5
    -3
      react-ui/src/pages/Experiment/Info/index.jsx
  11. +5
    -5
      react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx
  12. +0
    -0
      react-ui/src/pages/Experiment/components/ExperimentInstanceList/index.less
  13. +16
    -29
      react-ui/src/pages/Experiment/components/ExperimentInstanceList/index.tsx
  14. +85
    -0
      react-ui/src/pages/Experiment/components/ExperimentInstanceList/instance.tsx
  15. +76
    -41
      react-ui/src/pages/Experiment/index.jsx
  16. +8
    -6
      react-ui/src/pages/HyperParameter/Instance/index.tsx
  17. +1
    -0
      react-ui/src/pages/Points/components/Statistics/index.less
  18. +10
    -3
      react-ui/src/pages/Points/components/Statistics/index.tsx
  19. +5
    -2
      react-ui/src/pages/Workspace/components/UserPoints/index.tsx
  20. +8
    -0
      react-ui/src/services/experiment/index.js
  21. +3
    -0
      react-ui/src/utils/constant.ts
  22. +16
    -0
      react-ui/src/utils/format.ts
  23. +0
    -9
      react-ui/src/utils/index.ts

+ 2
- 0
react-ui/src/app.tsx View File

@@ -11,6 +11,7 @@ import { getAccessToken } from './access';
import ErrorBoundary from './components/ErrorBoundary';
import './dayjsConfig';
import { removeAllPageCacheState } from './hooks/useCacheState';
import { globalGetSeverTime } from './hooks/useServerTime';
import {
getRemoteMenu,
getRoutersInfo,
@@ -29,6 +30,7 @@ export { requestConfig as request } from './requestConfig';
export async function getInitialState(): Promise<GlobalInitialState> {
const fetchUserInfo = async () => {
try {
globalGetSeverTime();
const response = await getUserInfo();
return {
...response.user,


+ 16
- 23
react-ui/src/hooks/useSSE.ts View File

@@ -1,11 +1,12 @@
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 params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`);
const evtSource = new EventSource(
@@ -18,11 +19,10 @@ export const useSSE = (onMessage: (data: any) => void) => {
return;
}
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);
};

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]);
};

+ 54
- 0
react-ui/src/hooks/useServerTime.ts View File

@@ -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;
}

+ 8
- 6
react-ui/src/pages/ActiveLearn/Instance/index.tsx View File

@@ -1,5 +1,6 @@
import KFIcon from '@/components/KFIcon';
import { AutoMLTaskType, ExperimentStatus } from '@/enums';
import { useServerTime } from '@/hooks/useServerTime';
import { getActiveLearnInsReq } from '@/services/activeLearn';
import { NodeStatus } from '@/types';
import { parseJsonText } from '@/utils';
@@ -35,7 +36,8 @@ function ActiveLearnInstance() {
const params = useParams();
const instanceId = safeInvoke(Number)(params.id);
const evtSourceRef = useRef<EventSource | null>(null);
const [currentTime, setCurrentTime] = useState<Date>();
const [now] = useServerTime();
const [currentTime, setCurrentTime] = useState<Date>(now());
const finish_time = workflowStatus?.finishedAt;

useEffect(() => {
@@ -54,13 +56,13 @@ function ActiveLearnInstance() {
setCurrentTime(new Date(finish_time));
} else {
const timer = setInterval(() => {
setCurrentTime(new Date());
setCurrentTime(now());
}, 1000);
return () => {
clearInterval(timer);
};
}
}, [finish_time]);
}, [finish_time, now]);

// 获取实验实例详情
const getExperimentInsInfo = async (isStatusDetermined: boolean) => {
@@ -122,13 +124,13 @@ function ActiveLearnInstance() {
if (dataJson) {
const nodes = dataJson?.result?.object?.status?.nodes;
if (nodes) {
// 节点
setNodes(nodes);

const workflowStatus = Object.values(nodes).find((node: any) =>
node.displayName.startsWith(NodePrefix),
) as NodeStatus;

// 节点
setNodes(nodes);

// 设置工作流状态
if (workflowStatus) {
setWorkflowStatus(workflowStatus);


+ 8
- 6
react-ui/src/pages/AutoML/Instance/index.tsx View File

@@ -1,5 +1,6 @@
import KFIcon from '@/components/KFIcon';
import { AutoMLTaskType, AutoMLType, ExperimentStatus } from '@/enums';
import { useServerTime } from '@/hooks/useServerTime';
import { getExperimentInsReq } from '@/services/autoML';
import { NodeStatus } from '@/types';
import { parseJsonText } from '@/utils';
@@ -35,7 +36,8 @@ function AutoMLInstance() {
const params = useParams();
const instanceId = safeInvoke(Number)(params.id);
const evtSourceRef = useRef<EventSource | null>(null);
const [currentTime, setCurrentTime] = useState<Date>();
const [now] = useServerTime();
const [currentTime, setCurrentTime] = useState<Date>(now());
const finish_time = workflowStatus?.finishedAt;

useEffect(() => {
@@ -54,13 +56,13 @@ function AutoMLInstance() {
setCurrentTime(new Date(finish_time));
} else {
const timer = setInterval(() => {
setCurrentTime(new Date());
setCurrentTime(now());
}, 1000);
return () => {
clearInterval(timer);
};
}
}, [finish_time]);
}, [finish_time, now]);

// 获取实验实例详情
const getExperimentInsInfo = async (isStatusDetermined: boolean) => {
@@ -126,13 +128,13 @@ function AutoMLInstance() {
if (dataJson) {
const nodes = dataJson?.result?.object?.status?.nodes;
if (nodes) {
// 节点
setNodes(nodes);

const workflowStatus = Object.values(nodes).find((node: any) =>
node.displayName.startsWith(NodePrefix),
) as NodeStatus;

// 节点
setNodes(nodes);

if (workflowStatus) {
setWorkflowStatus(workflowStatus);



react-ui/src/pages/AutoML/components/ExperimentInstance/index.less → react-ui/src/pages/AutoML/components/ExperimentInstanceList/index.less View File


react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx → react-ui/src/pages/AutoML/components/ExperimentInstanceList/index.tsx View File

@@ -1,20 +1,19 @@
import KFIcon from '@/components/KFIcon';
import { ExperimentStatus } from '@/enums';
import { useCheck } from '@/hooks/useCheck';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import themes from '@/styles/theme.less';
import { type ExperimentInstance } from '@/types';
import { elapsedTime, formatDate } from '@/utils/date';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { DoubleRightOutlined } from '@ant-design/icons';
import { App, Button, Checkbox, ConfigProvider, Typography } from 'antd';
import { App, Button, Checkbox, ConfigProvider } from 'antd';
import classNames from 'classnames';
import { useEffect, useMemo } from 'react';
import { ExperimentListType, experimentListConfig } from '../ExperimentList/config';
import styles from './index.less';
import ExperimentInstanceComponent from './instance';

type ExperimentInstanceProps = {
type ExperimentInstanceListProps = {
type: ExperimentListType;
experimentInsList?: ExperimentInstance[];
experimentInsTotal: number;
@@ -24,7 +23,7 @@ type ExperimentInstanceProps = {
onLoadMore?: () => void;
};

function ExperimentInstanceComponent({
function ExperimentInstanceList({
type,
experimentInsList,
experimentInsTotal,
@@ -32,7 +31,7 @@ function ExperimentInstanceComponent({
onRemove,
onTerminate,
onLoadMore,
}: ExperimentInstanceProps) {
}: ExperimentInstanceListProps) {
const { message } = App.useApp();
const allIntanceIds = useMemo(() => {
return experimentInsList?.map((item) => item.id) || [];
@@ -171,28 +170,14 @@ function ExperimentInstanceComponent({
>
{index + 1}
</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}>
<Button
type="link"
@@ -244,4 +229,4 @@ function ExperimentInstanceComponent({
);
}

export default ExperimentInstanceComponent;
export default ExperimentInstanceList;

+ 83
- 0
react-ui/src/pages/AutoML/components/ExperimentInstanceList/instance.tsx View File

@@ -0,0 +1,83 @@
import { ExperimentStatus } from '@/enums';
import { useServerTime } from '@/hooks/useServerTime';
import { useSSE, type MessageHandler } from '@/hooks/useSSE';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import { ExperimentCompleted } from '@/utils/constant';
import { elapsedTime, formatDate } from '@/utils/date';
import { Typography } from 'antd';
import React, { useCallback, useEffect, useState } 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 [now] = useServerTime();
const [currentTime, setCurrentTime] = useState<Date>(now());

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);

// 定时刷新耗时
useEffect(() => {
if (finish_time) {
setCurrentTime(new Date(finish_time));
} else {
const timer = setInterval(() => {
setCurrentTime(now());
}, 1000);
return () => {
clearInterval(timer);
};
}
}, [finish_time, now]);

return (
<React.Fragment>
<div className={styles.description}>{elapsedTime(create_time, currentTime)}</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;

+ 80
- 41
react-ui/src/pages/AutoML/components/ExperimentList/index.tsx View File

@@ -12,6 +12,7 @@ import { AutoMLData } from '@/pages/AutoML/types';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import themes from '@/styles/theme.less';
import { type ExperimentInstance as ExperimentInstanceData } from '@/types';
import { ExperimentCompleted } from '@/utils/constant';
import { to } from '@/utils/promise';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
@@ -29,7 +30,7 @@ import {
import { type SearchProps } from 'antd/es/input';
import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react';
import ExperimentInstance from '../ExperimentInstance';
import ExperimentInstanceList from '../ExperimentInstanceList';
import { ExperimentListType, experimentListConfig } from './config';
import styles from './index.less';

@@ -78,6 +79,79 @@ function ExperimentList({ type }: ExperimentListProps) {
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,
),
);
setTimeout(() => {
refreshExperimentList();
}, 10000);
}
};

window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
}, [refreshExperimentList]);

// 搜索
const onSearch: SearchProps['onSearch'] = (value) => {
setSearchText(value);
@@ -151,40 +225,17 @@ function ExperimentList({ type }: ExperimentListProps) {
message.success('运行成功');
setExpandedRowKeys([record.id]);
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) => {
setExperimentInsList([]);
if (expanded) {
setExpandedRowKeys([record.id]);
getExperimentInsList(record.id, 0);
getExperimentInsList(record.id, 0, 5);
refreshExperimentList();
} else {
setExpandedRowKeys([]);
@@ -196,16 +247,11 @@ function ExperimentList({ type }: ExperimentListProps) {
navigate(`instance/${autoML.id}/${record.id}`);
};

// 刷新实验实例列表
const refreshExperimentIns = (experimentId: number) => {
getExperimentInsList(experimentId, 0);
};

// 加载更多实验实例
const loadMoreExperimentIns = () => {
const page = Math.round(experimentInsList.length / 5);
const recordId = expandedRowKeys[0];
getExperimentInsList(recordId, page);
getExperimentInsList(recordId, page, 5);
};

// 实验实例终止
@@ -224,13 +270,6 @@ function ExperimentList({ type }: ExperimentListProps) {
});
});
};

// 刷新实验列表状态,
// 目前是直接刷新实验列表,后续需要优化,只刷新状态
const refreshExperimentList = () => {
getAutoMLList();
};

// --------------------------- Table ---------------------------
// 分页切换
const handleTableChange: TableProps<AutoMLData>['onChange'] = (
@@ -409,7 +448,7 @@ function ExperimentList({ type }: ExperimentListProps) {
onChange={handleTableChange}
expandable={{
expandedRowRender: (record) => (
<ExperimentInstance
<ExperimentInstanceList
type={type}
experimentInsList={experimentInsList}
experimentInsTotal={experimentInsTotal}
@@ -420,7 +459,7 @@ function ExperimentList({ type }: ExperimentListProps) {
}}
onTerminate={handleInstanceTerminate}
onLoadMore={() => loadMoreExperimentIns()}
></ExperimentInstance>
></ExperimentInstanceList>
),
onExpand: handleExpandChange,
expandedRowKeys: expandedRowKeys,


+ 5
- 3
react-ui/src/pages/Experiment/Info/index.jsx View File

@@ -15,6 +15,7 @@ import ExperimentDrawer from '../components/ExperimentDrawer';
import ParamsModal from '../components/ViewParamsModal';
import { experimentStatusInfo } from '../status';
import styles from './index.less';
import { useServerTime } from '@/hooks/useServerTime';

let graph = null;

@@ -27,7 +28,8 @@ function ExperimentText() {
const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false);
const [propsDrawerOpen, openPropsDrawer, closePropsDrawer, propsDrawerOpenRef] =
useVisible(false);
const [currentTime, setCurrentTime] = useState();
const [now] = useServerTime();
const [currentTime, setCurrentTime] = useState(now());
const navigate = useNavigate();
const evtSourceRef = useRef();
const width = 110;
@@ -67,13 +69,13 @@ function ExperimentText() {
setCurrentTime(new Date(finish_time));
} else {
const timer = setInterval(() => {
setCurrentTime(new Date());
setCurrentTime(now());
}, 1000);
return () => {
clearInterval(timer);
};
}
}, [finish_time]);
}, [finish_time, now]);

// 获取流水线模版
const getWorkflow = async () => {


+ 5
- 5
react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx View File

@@ -1,4 +1,5 @@
import { ExperimentStatus } from '@/enums';
import { useServerTime } from '@/hooks/useServerTime';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import { PipelineNodeModelSerialize } from '@/types';
import { elapsedTime, formatDate } from '@/utils/date';
@@ -41,9 +42,8 @@ const ExperimentDrawer = ({
instanceNodeStartTime,
instanceNodeEndTime,
}: ExperimentDrawerProps) => {
const [currentTime, setCurrentTime] = useState(
instanceNodeEndTime ? new Date(instanceNodeEndTime) : new Date(),
);
const [now] = useServerTime();
const [currentTime, setCurrentTime] = useState(now());

// 定时刷新耗时
useEffect(() => {
@@ -51,13 +51,13 @@ const ExperimentDrawer = ({
setCurrentTime(new Date(instanceNodeEndTime));
} else {
const timer = setInterval(() => {
setCurrentTime(new Date());
setCurrentTime(now());
}, 1000);
return () => {
clearInterval(timer);
};
}
}, [instanceNodeEndTime]);
}, [instanceNodeEndTime, now]);

// 如果性能有问题,可以进一步拆解
const items = useMemo(


react-ui/src/pages/Experiment/components/ExperimentInstance/index.less → react-ui/src/pages/Experiment/components/ExperimentInstanceList/index.less View File


react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx → react-ui/src/pages/Experiment/components/ExperimentInstanceList/index.tsx View File

@@ -1,7 +1,6 @@
import KFIcon from '@/components/KFIcon';
import { ExperimentStatus } from '@/enums';
import { useCheck } from '@/hooks/useCheck';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import {
deleteManyExperimentIns,
deleteQueryByExperimentInsId,
@@ -9,17 +8,17 @@ import {
} from '@/services/experiment/index.js';
import themes from '@/styles/theme.less';
import { type ExperimentInstance } from '@/types';
import { elapsedTime, formatDate } from '@/utils/date';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { DoubleRightOutlined } from '@ant-design/icons';
import { App, Button, Checkbox, ConfigProvider, Typography } from 'antd';
import { App, Button, Checkbox, ConfigProvider } from 'antd';
import classNames from 'classnames';
import { useEffect, useMemo } from 'react';
import TensorBoardStatusCell from '../TensorBoardStatus';
import styles from './index.less';
import ExperimentInstanceComponent from './instance';

type ExperimentInstanceProps = {
type ExperimentInstanceListProps = {
experimentInsList?: ExperimentInstance[];
experimentInsTotal: number;
onClickInstance?: (instance: ExperimentInstance) => void;
@@ -29,7 +28,7 @@ type ExperimentInstanceProps = {
onLoadMore?: () => void;
};

function ExperimentInstanceComponent({
function ExperimentInstanceList({
experimentInsList,
experimentInsTotal,
onClickInstance,
@@ -37,7 +36,7 @@ function ExperimentInstanceComponent({
onRemove,
onTerminate,
onLoadMore,
}: ExperimentInstanceProps) {
}: ExperimentInstanceListProps) {
const { message } = App.useApp();
const allIntanceIds = useMemo(() => {
return experimentInsList?.map((item) => item.id) || [];
@@ -185,28 +184,16 @@ function ExperimentInstanceComponent({
'--'
)}
</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}>
<Button
type="link"
@@ -258,4 +245,4 @@ function ExperimentInstanceComponent({
);
}

export default ExperimentInstanceComponent;
export default ExperimentInstanceList;

+ 85
- 0
react-ui/src/pages/Experiment/components/ExperimentInstanceList/instance.tsx View File

@@ -0,0 +1,85 @@
import { ExperimentStatus } from '@/enums';
import { useServerTime } from '@/hooks/useServerTime';
import { useSSE, type MessageHandler } from '@/hooks/useSSE';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import { ExperimentCompleted } from '@/utils/constant';
import { elapsedTime, formatDate } from '@/utils/date';
import { Typography } from 'antd';
import React, { useCallback, useEffect, useState } 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 [now] = useServerTime();
const [currentTime, setCurrentTime] = useState<Date>(now());

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);

// 定时刷新耗时
useEffect(() => {
if (finish_time) {
setCurrentTime(new Date(finish_time));
} else {
const timer = setInterval(() => {
setCurrentTime(now());
}, 1000);
return () => {
clearInterval(timer);
};
}
}, [finish_time, now]);

return (
<React.Fragment>
<div className={styles.description}>
<div style={{ width: '50%' }}>{elapsedTime(create_time, currentTime)}</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;

+ 76
- 41
react-ui/src/pages/Experiment/index.jsx View File

@@ -15,6 +15,7 @@ import {
} from '@/services/experiment/index.js';
import { getWorkflow } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less';
import { ExperimentCompleted } from '@/utils/constant';
import { to } from '@/utils/promise';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
@@ -24,7 +25,7 @@ import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ComparisonType } from './Comparison/config';
import AddExperimentModal from './components/AddExperimentModal';
import ExperimentInstance from './components/ExperimentInstance';
import ExperimentInstanceList from './components/ExperimentInstanceList';
import styles from './index.less';
import { experimentStatusInfo } from './status';

@@ -36,7 +37,7 @@ function Experiment() {
const [experimentList, setExperimentList] = useState([]);
const [workflowList, setWorkflowList] = useState([]);
const [experimentId, setExperimentId] = useState(null);
const [experimentInList, setExperimentInList] = useState([]);
const [experimentInsList, setExperimentInsList] = useState([]);
const [expandedRowKeys, setExpandedRowKeys] = useState(null);
const [total, setTotal] = useState(0);
const [isAdd, setIsAdd] = useState(true);
@@ -54,6 +55,32 @@ function Experiment() {
);
const { message } = App.useApp();

// 获取实验列表
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(() => {
// 获取流水线列表
const getWorkflowList = async () => {
@@ -76,28 +103,41 @@ 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(() => {
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,
),
);
setTimeout(() => {
refreshExperimentList();
}, 10000);
}
};

window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
}, [refreshExperimentList]);

// 搜索
const onSearch = (value) => {
setSearchText(value);
@@ -108,11 +148,11 @@ function Experiment() {
};

// 获取实验实例列表
const getQueryByExperiment = async (experimentId, page) => {
const getQueryByExperiment = async (experimentId, page, size = 5) => {
const params = {
experimentId: experimentId,
page: page,
size: 5,
size: size,
};
const [res, error] = await to(getQueryByExperimentId(params));
if (res && res.data) {
@@ -127,10 +167,10 @@ function Experiment() {
};
});
if (page === 0) {
setExperimentInList(list);
setExperimentInsList(list);
clearExperimentInTimers();
} else {
setExperimentInList((prev) => [...prev, ...list]);
setExperimentInsList((prev) => [...prev, ...list]);
}
setExperimentInsTotal(totalElements);
// 获取 TensorBoard 状态
@@ -173,7 +213,7 @@ function Experiment() {
};
const [res] = await to(getTensorBoardStatusReq(params));
if (res && res.data) {
setExperimentInList((prevList) => {
setExperimentInsList((prevList) => {
return prevList.map((item) => {
if (item.id === experimentIn.id) {
return {
@@ -201,11 +241,11 @@ function Experiment() {
// 展开实例
const expandChange = (e, record) => {
clearExperimentInTimers();
setExperimentInList([]);
setExperimentInsList([]);
if (record.id === expandedRowKeys) {
setExpandedRowKeys(null);
} else {
getQueryByExperiment(record.id, 0);
getQueryByExperiment(record.id, 0, 5);
refreshExperimentList();
}
};
@@ -285,7 +325,7 @@ function Experiment() {
if (res) {
message.success('运行成功');
refreshExperimentList();
refreshExperimentIns(id);
getQueryByExperiment(id, 0, 5);
}
};

@@ -323,17 +363,11 @@ function Experiment() {
}
};

// 刷新实验列表状态,
// 目前是直接刷新实验列表,后续需要优化,只刷新状态
const refreshExperimentList = () => {
getExperimentList();
};

// 实验实例终止
const handleInstanceTerminate = async (experimentIn) => {
// 刷新实验列表
refreshExperimentList();
setExperimentInList((prevList) => {
setExperimentInsList((prevList) => {
return prevList.map((item) => {
if (item.id === experimentIn.id) {
return {
@@ -367,13 +401,14 @@ function Experiment() {

// 刷新实验实例列表
const refreshExperimentIns = (experimentId) => {
getQueryByExperiment(experimentId, 0);
const length = experimentInsList.length;
getQueryByExperiment(experimentId, 0, length);
};

// 加载更多实验实例
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 +579,8 @@ function Experiment() {
scroll={{ y: 'calc(100% - 55px)' }}
expandable={{
expandedRowRender: (record) => (
<ExperimentInstance
experimentInsList={experimentInList}
<ExperimentInstanceList
experimentInsList={experimentInsList}
experimentInsTotal={experimentInsTotal}
onClickInstance={(item) => gotoInstanceInfo(item, record)}
onClickTensorBoard={handleTensorboard}
@@ -555,7 +590,7 @@ function Experiment() {
}}
onTerminate={handleInstanceTerminate}
onLoadMore={() => loadMoreExperimentIns()}
></ExperimentInstance>
></ExperimentInstanceList>
),
onExpand: expandChange,
expandedRowKeys: [expandedRowKeys],


+ 8
- 6
react-ui/src/pages/HyperParameter/Instance/index.tsx View File

@@ -1,5 +1,6 @@
import KFIcon from '@/components/KFIcon';
import { ExperimentStatus } from '@/enums';
import { useServerTime } from '@/hooks/useServerTime';
import { getRayInsReq } from '@/services/hyperParameter';
import { NodeStatus } from '@/types';
import { parseJsonText } from '@/utils';
@@ -35,7 +36,8 @@ function HyperParameterInstance() {
const params = useParams();
const instanceId = safeInvoke(Number)(params.id);
const evtSourceRef = useRef<EventSource | null>(null);
const [currentTime, setCurrentTime] = useState<Date>();
const [now] = useServerTime();
const [currentTime, setCurrentTime] = useState<Date>(now());
const finish_time = workflowStatus?.finishedAt;

useEffect(() => {
@@ -54,13 +56,13 @@ function HyperParameterInstance() {
setCurrentTime(new Date(finish_time));
} else {
const timer = setInterval(() => {
setCurrentTime(new Date());
setCurrentTime(now());
}, 1000);
return () => {
clearInterval(timer);
};
}
}, [finish_time]);
}, [finish_time, now]);

// 获取实验实例详情
const getExperimentInsInfo = async (isStatusDetermined: boolean) => {
@@ -137,13 +139,13 @@ function HyperParameterInstance() {
if (dataJson) {
const nodes = dataJson?.result?.object?.status?.nodes;
if (nodes) {
// 节点
setNodes(nodes);

const workflowStatus = Object.values(nodes).find((node: any) =>
node.displayName.startsWith(NodePrefix),
) as NodeStatus;

// 节点
setNodes(nodes);

// 设置工作流状态
if (workflowStatus) {
setWorkflowStatus(workflowStatus);


+ 1
- 0
react-ui/src/pages/Points/components/Statistics/index.less View File

@@ -11,6 +11,7 @@
flex: 1;
flex-direction: column;
align-items: center;
padding: 0 20px;

&--border {
border-right: 1px solid @border-color;


+ 10
- 3
react-ui/src/pages/Points/components/Statistics/index.tsx View File

@@ -1,3 +1,5 @@
import { formatNumber } from '@/utils/format';
import { Typography } from 'antd';
import classNames from 'classnames';
import styles from './index.less';

@@ -10,11 +12,11 @@ function Statistics({ remaining, consuming }: StatisticsProps) {
const items = [
{
title: '当前可用算力积分(分)',
value: remaining ?? '-',
value: remaining,
},
{
title: '总消耗算力积分(分)',
value: consuming ?? '-',
value: consuming,
},
];

@@ -27,7 +29,12 @@ function Statistics({ remaining, consuming }: StatisticsProps) {
[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>
</div>
))}


+ 5
- 2
react-ui/src/pages/Workspace/components/UserPoints/index.tsx View File

@@ -1,5 +1,6 @@
import { PointsStatistics } from '@/pages/Points/index';
import { getPointsStatisticsReq } from '@/services/points';
import { formatNumber } from '@/utils/format';
import { to } from '@/utils/promise';
import { useNavigate } from '@umijs/max';
import { Typography } from 'antd';
@@ -22,14 +23,16 @@ function UserPoints() {
getPointsStatistics();
}, []);

const userCredit = formatNumber(statistics?.userCredit);

return (
<div className={styles['user-points']}>
<div className={styles['user-points__label']}>当前可用算力积分</div>
<Typography.Paragraph
className={styles['user-points__value']}
ellipsis={{ tooltip: statistics?.userCredit ?? '--' }}
ellipsis={{ tooltip: userCredit }}
>
{statistics?.userCredit ?? '--'}
{userCredit}
</Typography.Paragraph>
<div
className={styles['user-points__button']}


+ 8
- 0
react-ui/src/services/experiment/index.js View File

@@ -151,3 +151,11 @@ export function getExpMetricsReq(data) {
data,
});
}

// 获取服务器的当前时间
export function getSeverTimeReq(data) {
return request(`/api/mmp/experimentIns/time`, {
method: 'GET',
});
}


+ 3
- 0
react-ui/src/utils/constant.ts View File

@@ -17,3 +17,6 @@ export const VersionChangedMessage = 'versionChanged';

// 创建服务成功消息,去创建服务版本
export const ServiceCreatedMessage = 'serviceCreated';

// 实验完成
export const ExperimentCompleted = 'ExperimentCompleted';

+ 16
- 0
react-ui/src/utils/format.ts View File

@@ -193,3 +193,19 @@ export const formatEnum = (options: EnumOptions[]): FormatEnumFunc => {
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
}

+ 0
- 9
react-ui/src/utils/index.ts View File

@@ -348,12 +348,3 @@ export const convertEmptyStringToUndefined = (value?: string): string | undefine
return value === '' ? undefined : value;
};


export const formatNumber = (value?: number | null, toFixed?: number) : number | string => {
if (typeof value !== "number") {
return '--'
}

return toFixed ? Number(value).toFixed(toFixed) : value
}


Loading…
Cancel
Save