| @@ -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, | |||
| @@ -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]); | |||
| }; | |||
| @@ -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; | |||
| } | |||
| @@ -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); | |||
| @@ -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); | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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, | |||
| @@ -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 () => { | |||
| @@ -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( | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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], | |||
| @@ -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); | |||
| @@ -11,6 +11,7 @@ | |||
| flex: 1; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| padding: 0 20px; | |||
| &--border { | |||
| border-right: 1px solid @border-color; | |||
| @@ -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> | |||
| ))} | |||
| @@ -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']} | |||
| @@ -151,3 +151,11 @@ export function getExpMetricsReq(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 ExperimentCompleted = 'ExperimentCompleted'; | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||